Skip to main content

mz_storage_controller/
lib.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//! Implementation of the storage controller trait.
11
12use std::any::Any;
13use std::collections::btree_map;
14use std::collections::{BTreeMap, BTreeSet};
15use std::fmt::Debug;
16use std::str::FromStr;
17use std::sync::{Arc, Mutex};
18use std::time::Duration;
19
20use crate::collection_mgmt::{
21    AppendOnlyIntrospectionConfig, CollectionManagerKind, DifferentialIntrospectionConfig,
22};
23use crate::instance::{Instance, ReplicaConfig};
24use async_trait::async_trait;
25use chrono::{DateTime, DurationRound, TimeDelta, Utc};
26use derivative::Derivative;
27use differential_dataflow::lattice::Lattice;
28use futures::FutureExt;
29use futures::StreamExt;
30use itertools::Itertools;
31use mz_build_info::BuildInfo;
32use mz_cluster_client::client::ClusterReplicaLocation;
33use mz_cluster_client::metrics::{ControllerMetrics, WallclockLagMetrics};
34use mz_cluster_client::{ReplicaId, WallclockLagFn};
35use mz_controller_types::dyncfgs::{
36    ENABLE_0DT_DEPLOYMENT_SOURCES, WALLCLOCK_LAG_RECORDING_INTERVAL,
37};
38use mz_dyncfg::ConfigUpdates;
39use mz_ore::collections::CollectionExt;
40use mz_ore::metrics::MetricsRegistry;
41use mz_ore::now::NowFn;
42use mz_ore::task::AbortOnDropHandle;
43use mz_ore::{assert_none, halt, instrument, restart_in_place, soft_panic_or_log};
44use mz_persist_client::batch::ProtoBatch;
45use mz_persist_client::cache::PersistClientCache;
46use mz_persist_client::cfg::USE_CRITICAL_SINCE_SNAPSHOT;
47use mz_persist_client::critical::Opaque;
48use mz_persist_client::read::ReadHandle;
49use mz_persist_client::schema::CaESchema;
50use mz_persist_client::write::WriteHandle;
51use mz_persist_client::{Diagnostics, PersistClient, PersistLocation, ShardId};
52use mz_persist_types::codec_impls::UnitSchema;
53use mz_repr::adt::timestamp::CheckedTimestamp;
54use mz_repr::{Datum, Diff, GlobalId, RelationDesc, RelationVersion, Row, Timestamp};
55use mz_storage_client::client::{
56    AppendOnlyUpdate, RunIngestionCommand, RunOneshotIngestion, RunSinkCommand, Status,
57    StatusUpdate, StorageCommand, StorageResponse, TableData,
58};
59use mz_storage_client::controller::{
60    BoxFuture, CollectionDescription, DataSource, ExportDescription, ExportState,
61    IntrospectionType, MonotonicAppender, PersistEpoch, Response, StorageController,
62    StorageMetadata, StorageTxn, StorageWriteOp, TableRegistration, WallclockLag,
63    WallclockLagHistogramPeriod,
64};
65use mz_storage_client::healthcheck::{
66    MZ_AWS_PRIVATELINK_CONNECTION_STATUS_HISTORY_DESC, MZ_SINK_STATUS_HISTORY_DESC,
67    MZ_SOURCE_STATUS_HISTORY_DESC, REPLICA_STATUS_HISTORY_DESC,
68};
69use mz_storage_client::metrics::StorageControllerMetrics;
70use mz_storage_client::statistics::{
71    ControllerSinkStatistics, ControllerSourceStatistics, WebhookStatistics,
72};
73use mz_storage_client::storage_collections::{StorageCollections, shard_has_no_updates};
74use mz_storage_types::configuration::StorageConfiguration;
75use mz_storage_types::connections::ConnectionContext;
76use mz_storage_types::connections::inline::InlinedConnection;
77use mz_storage_types::controller::{AlterError, CollectionMetadata, StorageError, TxnsCodecRow};
78use mz_storage_types::errors::CollectionMissing;
79use mz_storage_types::instances::StorageInstanceId;
80use mz_storage_types::oneshot_sources::{OneshotIngestionRequest, OneshotResultCallback};
81use mz_storage_types::parameters::StorageParameters;
82use mz_storage_types::read_holds::ReadHold;
83use mz_storage_types::read_policy::ReadPolicy;
84use mz_storage_types::sinks::{StorageSinkConnection, StorageSinkDesc};
85use mz_storage_types::sources::{
86    GenericSourceConnection, IngestionDescription, SourceConnection, SourceData, SourceDesc,
87    SourceExport, SourceExportDataConfig,
88};
89use mz_storage_types::{AlterCompatible, StorageDiff, dyncfgs};
90use mz_txn_wal::metrics::Metrics as TxnMetrics;
91use mz_txn_wal::txn_read::TxnsRead;
92use mz_txn_wal::txns::TxnsHandle;
93use timely::order::PartialOrder;
94use timely::progress::frontier::MutableAntichain;
95use timely::progress::{Antichain, ChangeBatch};
96use tokio::sync::watch::{Sender, channel};
97use tokio::sync::{mpsc, oneshot};
98use tokio::time::MissedTickBehavior;
99use tokio::time::error::Elapsed;
100use tracing::{debug, info, warn};
101
102mod collection_mgmt;
103mod history;
104mod instance;
105mod persist_handles;
106mod rtr;
107mod statistics;
108
109#[derive(Derivative)]
110#[derivative(Debug)]
111struct PendingOneshotIngestion {
112    /// Callback used to provide results of the ingestion.
113    #[derivative(Debug = "ignore")]
114    result_tx: OneshotResultCallback<ProtoBatch>,
115    /// Cluster currently running this ingestion
116    cluster_id: StorageInstanceId,
117}
118
119impl PendingOneshotIngestion {
120    /// Consume the pending ingestion, responding with a cancelation message.
121    ///
122    /// TODO(cf2): Refine these error messages so they're not stringly typed.
123    pub(crate) fn cancel(self) {
124        (self.result_tx)(vec![Err("canceled".to_string())])
125    }
126}
127
128/// A storage controller for a storage instance.
129#[derive(Derivative)]
130#[derivative(Debug)]
131pub struct Controller {
132    /// The build information for this process.
133    build_info: &'static BuildInfo,
134    /// A function that returns the current time.
135    now: NowFn,
136
137    /// Whether or not this controller is in read-only mode.
138    ///
139    /// When in read-only mode, neither this controller nor the instances
140    /// controlled by it are allowed to affect changes to external systems
141    /// (largely persist).
142    read_only: bool,
143
144    /// Collections maintained by the storage controller.
145    ///
146    /// This collection only grows, although individual collections may be rendered unusable.
147    /// This is to prevent the re-binding of identifiers to other descriptions.
148    pub(crate) collections: BTreeMap<GlobalId, CollectionState>,
149
150    /// Map from IDs of objects that have been dropped to replicas we are still
151    /// expecting DroppedId messages from. This is cleared out once all replicas
152    /// have responded.
153    ///
154    /// We use this only to catch problems in the protocol between controller
155    /// and replicas, for example we can differentiate between late messages for
156    /// objects that have already been dropped and unexpected (read erroneous)
157    /// messages from the replica.
158    dropped_objects: BTreeMap<GlobalId, BTreeSet<ReplicaId>>,
159
160    /// Write handle for table shards.
161    pub(crate) persist_table_worker: persist_handles::PersistTableWriteWorker,
162    /// A shared TxnsCache running in a task and communicated with over a channel.
163    txns_read: TxnsRead<Timestamp>,
164    txns_metrics: Arc<TxnMetrics>,
165    stashed_responses: Vec<(Option<ReplicaId>, StorageResponse)>,
166    /// Channel for sending table handle drops.
167    #[derivative(Debug = "ignore")]
168    pending_table_handle_drops_tx: mpsc::UnboundedSender<GlobalId>,
169    /// Channel for receiving table handle drops.
170    #[derivative(Debug = "ignore")]
171    pending_table_handle_drops_rx: mpsc::UnboundedReceiver<GlobalId>,
172    /// Closures that can be used to send responses from oneshot ingestions.
173    #[derivative(Debug = "ignore")]
174    pending_oneshot_ingestions: BTreeMap<uuid::Uuid, PendingOneshotIngestion>,
175
176    /// Interface for managed collections
177    pub(crate) collection_manager: collection_mgmt::CollectionManager,
178
179    /// Tracks which collection is responsible for which [`IntrospectionType`].
180    pub(crate) introspection_ids: BTreeMap<IntrospectionType, GlobalId>,
181    /// Tokens for tasks that drive updating introspection collections. Dropping
182    /// this will make sure that any tasks (or other resources) will stop when
183    /// needed.
184    // TODO(aljoscha): Should these live somewhere else?
185    introspection_tokens: Arc<Mutex<BTreeMap<GlobalId, Box<dyn Any + Send + Sync>>>>,
186
187    // The following two fields must always be locked in order.
188    /// Consolidated metrics updates to periodically write. We do not eagerly initialize this,
189    /// and its contents are entirely driven by `StorageResponse::StatisticsUpdates`'s, as well
190    /// as webhook statistics.
191    source_statistics: Arc<Mutex<statistics::SourceStatistics>>,
192    /// Consolidated metrics updates to periodically write. We do not eagerly initialize this,
193    /// and its contents are entirely driven by `StorageResponse::StatisticsUpdates`'s.
194    sink_statistics: Arc<Mutex<BTreeMap<(GlobalId, Option<ReplicaId>), ControllerSinkStatistics>>>,
195    /// A way to update the statistics interval in the statistics tasks.
196    statistics_interval_sender: Sender<Duration>,
197
198    /// Clients for all known storage instances.
199    instances: BTreeMap<StorageInstanceId, Instance>,
200    /// Per-replica dyncfg overrides, consulted when a replica connects. The
201    /// overrides for a replica created by DDL are committed in the same
202    /// transaction that creates it, so they are installed before the replica
203    /// connects.
204    replica_dyncfg_overrides: BTreeMap<ReplicaId, ConfigUpdates>,
205    /// Set to `true` once `initialization_complete` has been called.
206    initialized: bool,
207    /// Storage configuration to apply to newly provisioned instances, and use during purification.
208    config: StorageConfiguration,
209    /// The persist location where all storage collections are being written to
210    persist_location: PersistLocation,
211    /// A persist client used to write to storage collections
212    persist: Arc<PersistClientCache>,
213    /// Metrics of the Storage controller
214    metrics: StorageControllerMetrics,
215    /// `(read, write)` frontiers that have been recorded in the `Frontiers` collection, kept to be
216    /// able to retract old rows.
217    recorded_frontiers: BTreeMap<GlobalId, (Antichain<Timestamp>, Antichain<Timestamp>)>,
218    /// Write frontiers that have been recorded in the `ReplicaFrontiers` collection, kept to be
219    /// able to retract old rows.
220    recorded_replica_frontiers: BTreeMap<(GlobalId, ReplicaId), Antichain<Timestamp>>,
221
222    /// A function that computes the lag between the given time and wallclock time.
223    #[derivative(Debug = "ignore")]
224    wallclock_lag: WallclockLagFn<Timestamp>,
225    /// The last time wallclock lag introspection was recorded.
226    wallclock_lag_last_recorded: DateTime<Utc>,
227
228    /// Handle to a [StorageCollections].
229    storage_collections: Arc<dyn StorageCollections + Send + Sync>,
230    /// Migrated storage collections that can be written even in read only mode.
231    migrated_storage_collections: BTreeSet<GlobalId>,
232
233    /// Ticker for scheduling periodic maintenance work.
234    maintenance_ticker: tokio::time::Interval,
235    /// Whether maintenance work was scheduled.
236    maintenance_scheduled: bool,
237
238    /// Shared transmit channel for replicas to send responses.
239    instance_response_tx: mpsc::UnboundedSender<(Option<ReplicaId>, StorageResponse)>,
240    /// Receive end for replica responses.
241    instance_response_rx: mpsc::UnboundedReceiver<(Option<ReplicaId>, StorageResponse)>,
242
243    /// Background task run at startup to warm persist state.
244    persist_warm_task: Option<AbortOnDropHandle<Box<dyn Debug + Send>>>,
245}
246
247/// Warm up persist state for `shard_ids` in a background task.
248///
249/// With better parallelism during startup this would likely be unnecessary, but empirically we see
250/// some nice speedups with this relatively simple function.
251/// What `create_collections_for_bootstrap` opens per collection: a write handle for collections
252/// the controller writes directly, or just a recent, linearized upper for txns-managed tables,
253/// whose writes go through the table-write worker (which opens its own handles per
254/// registration).
255enum WriteHandleOrUpper {
256    Handle(WriteHandle<SourceData, (), Timestamp, StorageDiff>),
257    Upper(Antichain<Timestamp>),
258}
259
260impl WriteHandleOrUpper {
261    fn upper(&self) -> Antichain<Timestamp> {
262        match self {
263            Self::Handle(handle) => handle.upper().clone(),
264            Self::Upper(upper) => upper.clone(),
265        }
266    }
267
268    /// Returns the write handle, panicking for the `Upper` variant. Only call for data sources
269    /// that are never tables.
270    fn expect_handle(self, context: &str) -> WriteHandle<SourceData, (), Timestamp, StorageDiff> {
271        match self {
272            Self::Handle(handle) => handle,
273            Self::Upper(_) => panic!("write handle required: {context}"),
274        }
275    }
276}
277
278fn warm_persist_state_in_background(
279    client: PersistClient,
280    shard_ids: impl Iterator<Item = ShardId> + Send + 'static,
281) -> mz_ore::task::JoinHandle<Box<dyn Debug + Send>> {
282    /// Bound the number of shards that we warm at a single time, to limit our overall resource use.
283    const MAX_CONCURRENT_WARMS: usize = 16;
284    let logic = async move {
285        let fetchers: Vec<_> = tokio_stream::iter(shard_ids)
286            .map(|shard_id| {
287                let client = client.clone();
288                async move {
289                    client
290                        .create_batch_fetcher::<SourceData, (), mz_repr::Timestamp, StorageDiff>(
291                            shard_id,
292                            Arc::new(RelationDesc::empty()),
293                            Arc::new(UnitSchema),
294                            true,
295                            Diagnostics::from_purpose("warm persist load state"),
296                        )
297                        .await
298                }
299            })
300            .buffer_unordered(MAX_CONCURRENT_WARMS)
301            .collect()
302            .await;
303        let fetchers: Box<dyn Debug + Send> = Box::new(fetchers);
304        fetchers
305    };
306    mz_ore::task::spawn(|| "warm_persist_load_state", logic)
307}
308
309#[async_trait(?Send)]
310impl StorageController for Controller {
311    fn initialization_complete(&mut self) {
312        self.reconcile_dangling_statistics();
313        self.initialized = true;
314
315        for instance in self.instances.values_mut() {
316            instance.send(StorageCommand::InitializationComplete);
317        }
318    }
319
320    fn update_parameters(&mut self, config_params: StorageParameters) {
321        self.storage_collections
322            .update_parameters(config_params.clone());
323
324        // We serialize the dyncfg updates in StorageParameters, but configure
325        // persist separately.
326        self.persist.cfg().apply_from(&config_params.dyncfg_updates);
327
328        for instance in self.instances.values_mut() {
329            let params = Box::new(config_params.clone());
330            instance.send(StorageCommand::UpdateConfiguration(params));
331        }
332        self.config.update(config_params);
333        self.statistics_interval_sender
334            .send_replace(self.config.parameters.statistics_interval);
335        self.collection_manager.update_user_batch_duration(
336            self.config
337                .parameters
338                .user_storage_managed_collections_batch_duration,
339        );
340    }
341
342    fn update_replica_dyncfg_overrides(
343        &mut self,
344        mut overrides: BTreeMap<StorageInstanceId, BTreeMap<ReplicaId, ConfigUpdates>>,
345    ) {
346        self.replica_dyncfg_overrides = overrides
347            .values()
348            .flat_map(|replicas| replicas.iter())
349            .map(|(replica_id, updates)| (*replica_id, updates.clone()))
350            .collect();
351        for (id, instance) in self.instances.iter_mut() {
352            let instance_overrides = overrides.remove(id).unwrap_or_default();
353            instance.update_replica_dyncfg_overrides(instance_overrides);
354        }
355    }
356
357    /// Get the current configuration
358    fn config(&self) -> &StorageConfiguration {
359        &self.config
360    }
361
362    fn collection_metadata(&self, id: GlobalId) -> Result<CollectionMetadata, CollectionMissing> {
363        self.storage_collections.collection_metadata(id)
364    }
365
366    fn collection_hydrated(&self, collection_id: GlobalId) -> Result<bool, StorageError> {
367        let collection = self.collection(collection_id)?;
368
369        let instance_id = match &collection.data_source {
370            DataSource::Ingestion(ingestion_description) => ingestion_description.instance_id,
371            DataSource::IngestionExport { ingestion_id, .. } => {
372                let ingestion_state = self.collections.get(ingestion_id).expect("known to exist");
373
374                let instance_id = match &ingestion_state.data_source {
375                    DataSource::Ingestion(ingestion_desc) => ingestion_desc.instance_id,
376                    _ => unreachable!("SourceExport must only refer to primary source"),
377                };
378
379                instance_id
380            }
381            _ => return Ok(true),
382        };
383
384        let instance = self.instances.get(&instance_id).ok_or_else(|| {
385            StorageError::IngestionInstanceMissing {
386                storage_instance_id: instance_id,
387                ingestion_id: collection_id,
388            }
389        })?;
390
391        if instance.replica_ids().next().is_none() {
392            // Ingestions on zero-replica clusters are always considered
393            // hydrated.
394            return Ok(true);
395        }
396
397        match &collection.extra_state {
398            CollectionStateExtra::Ingestion(ingestion_state) => {
399                // An ingestion is hydrated if it is hydrated on at least one replica.
400                Ok(ingestion_state.hydrated_on.len() >= 1)
401            }
402            CollectionStateExtra::Export(_) => {
403                // For now, sinks are always considered hydrated. We rely on
404                // them starting up "instantly" and don't wait for them when
405                // checking hydration status of a replica.  TODO(sinks): base
406                // this off of the sink shard's frontier?
407                Ok(true)
408            }
409            CollectionStateExtra::None => {
410                // For now, objects that are not ingestions are always
411                // considered hydrated. This is tables and webhooks, as of
412                // today.
413                Ok(true)
414            }
415        }
416    }
417
418    #[mz_ore::instrument(level = "debug")]
419    fn collections_hydrated_on_replicas(
420        &self,
421        target_replica_ids: Option<Vec<ReplicaId>>,
422        target_cluster_id: &StorageInstanceId,
423        exclude_collections: &BTreeSet<GlobalId>,
424    ) -> Result<bool, StorageError> {
425        // If an empty set of replicas is provided there can be
426        // no collections on them, we'll count this as hydrated.
427        if target_replica_ids.as_ref().is_some_and(|v| v.is_empty()) {
428            return Ok(true);
429        }
430
431        // If target_replica_ids is provided, use it as the set of target
432        // replicas. Otherwise check for hydration on any replica.
433        let target_replicas: Option<BTreeSet<ReplicaId>> =
434            target_replica_ids.map(|ids| ids.into_iter().collect());
435
436        let instance = self.instances.get(target_cluster_id);
437
438        let mut all_hydrated = true;
439        for (collection_id, collection_state) in self.collections.iter() {
440            if collection_id.is_transient() || exclude_collections.contains(collection_id) {
441                continue;
442            }
443            let hydrated = match &collection_state.extra_state {
444                CollectionStateExtra::Ingestion(state) => {
445                    if &state.instance_id != target_cluster_id {
446                        continue;
447                    }
448                    match &target_replicas {
449                        Some(target_replicas) => {
450                            // Not scheduled on any target replica means it can
451                            // never hydrate there, so it does not count (see
452                            // the trait docs). If the instance is unknown (the
453                            // cluster is being dropped concurrently) the
454                            // scheduled set is empty and every ingestion is
455                            // skipped. Readiness callers gate on replica health
456                            // separately, which covers that window.
457                            let scheduled_on = instance
458                                .map(|i| i.get_active_replicas_for_object(collection_id))
459                                .unwrap_or_default();
460                            if scheduled_on.is_disjoint(target_replicas) {
461                                true
462                            } else {
463                                !state.hydrated_on.is_disjoint(target_replicas)
464                            }
465                        }
466                        None => {
467                            // Not target replicas, so check that it's hydrated
468                            // on at least one replica.
469                            state.hydrated_on.len() >= 1
470                        }
471                    }
472                }
473                CollectionStateExtra::Export(_) => {
474                    // For now, sinks are always considered hydrated. We rely on
475                    // them starting up "instantly" and don't wait for them when
476                    // checking hydration status of a replica.  TODO(sinks):
477                    // base this off of the sink shard's frontier?
478                    true
479                }
480                CollectionStateExtra::None => {
481                    // For now, objects that are not ingestions are always
482                    // considered hydrated. This is tables and webhooks, as of
483                    // today.
484                    true
485                }
486            };
487            if !hydrated {
488                tracing::info!(%collection_id, "collection is not hydrated on any replica");
489                all_hydrated = false;
490                // We continue with our loop instead of breaking out early, so
491                // that we log all non-hydrated replicas.
492            }
493        }
494        Ok(all_hydrated)
495    }
496
497    fn collection_frontiers(
498        &self,
499        id: GlobalId,
500    ) -> Result<(Antichain<Timestamp>, Antichain<Timestamp>), CollectionMissing> {
501        let frontiers = self.storage_collections.collection_frontiers(id)?;
502        Ok((frontiers.implied_capability, frontiers.write_frontier))
503    }
504
505    fn collections_frontiers(
506        &self,
507        mut ids: Vec<GlobalId>,
508    ) -> Result<Vec<(GlobalId, Antichain<Timestamp>, Antichain<Timestamp>)>, CollectionMissing>
509    {
510        let mut result = vec![];
511        // In theory, we could pull all our frontiers from storage collections...
512        // but in practice those frontiers may not be identical. For historical reasons, we use the
513        // locally-tracked frontier for sinks but the storage-collections-maintained frontier for
514        // sources.
515        ids.retain(|&id| match self.export(id) {
516            Ok(export) => {
517                result.push((
518                    id,
519                    export.input_hold().since().clone(),
520                    export.write_frontier.clone(),
521                ));
522                false
523            }
524            Err(_) => true,
525        });
526        result.extend(
527            self.storage_collections
528                .collections_frontiers(ids)?
529                .into_iter()
530                .map(|frontiers| {
531                    (
532                        frontiers.id,
533                        frontiers.implied_capability,
534                        frontiers.write_frontier,
535                    )
536                }),
537        );
538
539        Ok(result)
540    }
541
542    fn active_collection_metadatas(&self) -> Vec<(GlobalId, CollectionMetadata)> {
543        self.storage_collections.active_collection_metadatas()
544    }
545
546    fn active_ingestion_exports(
547        &self,
548        instance_id: StorageInstanceId,
549    ) -> Box<dyn Iterator<Item = &GlobalId> + '_> {
550        let active_storage_collections: BTreeMap<_, _> = self
551            .storage_collections
552            .active_collection_frontiers()
553            .into_iter()
554            .map(|c| (c.id, c))
555            .collect();
556
557        let active_exports = self.instances[&instance_id]
558            .active_ingestion_exports()
559            .filter(move |id| {
560                let frontiers = active_storage_collections.get(id);
561                match frontiers {
562                    Some(frontiers) => !frontiers.write_frontier.is_empty(),
563                    None => {
564                        // Not "active", so we don't care here.
565                        false
566                    }
567                }
568            });
569
570        Box::new(active_exports)
571    }
572
573    fn check_exists(&self, id: GlobalId) -> Result<(), StorageError> {
574        self.storage_collections.check_exists(id)
575    }
576
577    fn create_instance(&mut self, id: StorageInstanceId, workload_class: Option<String>) {
578        let metrics = self.metrics.for_instance(id);
579        let mut instance = Instance::new(
580            workload_class,
581            metrics,
582            self.now.clone(),
583            self.instance_response_tx.clone(),
584        );
585        if self.initialized {
586            instance.send(StorageCommand::InitializationComplete);
587        }
588        if !self.read_only {
589            instance.send(StorageCommand::AllowWrites);
590        }
591
592        let params = Box::new(self.config.parameters.clone());
593        instance.send(StorageCommand::UpdateConfiguration(params));
594
595        let old_instance = self.instances.insert(id, instance);
596        assert_none!(old_instance, "storage instance {id} already exists");
597    }
598
599    fn drop_instance(&mut self, id: StorageInstanceId) {
600        let instance = self.instances.remove(&id);
601        assert!(instance.is_some(), "storage instance {id} does not exist");
602    }
603
604    fn update_instance_workload_class(
605        &mut self,
606        id: StorageInstanceId,
607        workload_class: Option<String>,
608    ) {
609        let instance = self
610            .instances
611            .get_mut(&id)
612            .unwrap_or_else(|| panic!("instance {id} does not exist"));
613
614        instance.workload_class = workload_class;
615    }
616
617    fn connect_replica(
618        &mut self,
619        instance_id: StorageInstanceId,
620        replica_id: ReplicaId,
621        location: ClusterReplicaLocation,
622    ) {
623        let instance = self
624            .instances
625            .get_mut(&instance_id)
626            .unwrap_or_else(|| panic!("instance {instance_id} does not exist"));
627
628        let config = ReplicaConfig {
629            build_info: self.build_info,
630            location,
631            grpc_client: self.config.parameters.grpc_client.clone(),
632        };
633        let dyncfg_override = self
634            .replica_dyncfg_overrides
635            .get(&replica_id)
636            .cloned()
637            .unwrap_or_default();
638        instance.add_replica(replica_id, config, dyncfg_override);
639    }
640
641    fn drop_replica(&mut self, instance_id: StorageInstanceId, replica_id: ReplicaId) {
642        // The coordinator only re-pushes the override map when the scoped
643        // configuration itself changes, so a dropped replica's entry would
644        // otherwise be retained until the next such change.
645        self.replica_dyncfg_overrides.remove(&replica_id);
646
647        let instance = self
648            .instances
649            .get_mut(&instance_id)
650            .unwrap_or_else(|| panic!("instance {instance_id} does not exist"));
651
652        let status_now = mz_ore::now::to_datetime((self.now)());
653        let mut source_status_updates = vec![];
654        let mut sink_status_updates = vec![];
655
656        // NOTE: mz_source_statuses and mz_sink_statuses rely on per-replica `paused` meaning
657        // "replica dropped"
658        let make_update = |id, object_type| StatusUpdate {
659            id,
660            status: Status::Paused,
661            timestamp: status_now,
662            error: None,
663            hints: BTreeSet::from([format!(
664                "The replica running this {object_type} has been dropped"
665            )]),
666            namespaced_errors: Default::default(),
667            replica_id: Some(replica_id),
668        };
669
670        for ingestion_id in instance.active_ingestions() {
671            if let Some(active_replicas) = self.dropped_objects.get_mut(ingestion_id) {
672                active_replicas.remove(&replica_id);
673                if active_replicas.is_empty() {
674                    self.dropped_objects.remove(ingestion_id);
675                }
676            }
677
678            let ingestion = self
679                .collections
680                .get_mut(ingestion_id)
681                .expect("instance contains unknown ingestion");
682
683            let ingestion_description = match &ingestion.data_source {
684                DataSource::Ingestion(ingestion_description) => ingestion_description.clone(),
685                _ => panic!(
686                    "unexpected data source for ingestion: {:?}",
687                    ingestion.data_source
688                ),
689            };
690
691            let old_style_ingestion = *ingestion_id != ingestion_description.remap_collection_id;
692            let subsource_ids = ingestion_description.collection_ids().filter(|id| {
693                // NOTE(aljoscha): We filter out the remap collection for old style
694                // ingestions because it doesn't get any status updates about it from the
695                // replica side. So we don't want to synthesize a 'paused' status here.
696                // New style ingestion do, since the source itself contains the remap data.
697                let should_discard =
698                    old_style_ingestion && id == &ingestion_description.remap_collection_id;
699                !should_discard
700            });
701            for id in subsource_ids {
702                source_status_updates.push(make_update(id, "source"));
703            }
704        }
705
706        for id in instance.active_exports() {
707            if let Some(active_replicas) = self.dropped_objects.get_mut(id) {
708                active_replicas.remove(&replica_id);
709                if active_replicas.is_empty() {
710                    self.dropped_objects.remove(id);
711                }
712            }
713
714            sink_status_updates.push(make_update(*id, "sink"));
715        }
716
717        instance.drop_replica(replica_id);
718
719        if !self.read_only {
720            if !source_status_updates.is_empty() {
721                self.append_status_introspection_updates(
722                    IntrospectionType::SourceStatusHistory,
723                    source_status_updates,
724                );
725            }
726            if !sink_status_updates.is_empty() {
727                self.append_status_introspection_updates(
728                    IntrospectionType::SinkStatusHistory,
729                    sink_status_updates,
730                );
731            }
732        }
733    }
734
735    async fn evolve_nullability_for_bootstrap(
736        &mut self,
737        storage_metadata: &StorageMetadata,
738        collections: Vec<(GlobalId, RelationDesc)>,
739    ) -> Result<(), StorageError> {
740        let persist_client = self
741            .persist
742            .open(self.persist_location.clone())
743            .await
744            .unwrap();
745
746        for (global_id, relation_desc) in collections {
747            let shard_id = storage_metadata.get_collection_shard(global_id)?;
748            let diagnostics = Diagnostics {
749                shard_name: global_id.to_string(),
750                handle_purpose: "evolve nullability for bootstrap".to_string(),
751            };
752            let latest_schema = persist_client
753                .latest_schema::<SourceData, (), Timestamp, StorageDiff>(shard_id, diagnostics)
754                .await
755                .expect("invalid persist usage");
756            let Some((schema_id, current_schema, _)) = latest_schema else {
757                tracing::debug!(?global_id, "no schema registered");
758                continue;
759            };
760            tracing::debug!(?global_id, ?current_schema, new_schema = ?relation_desc, "migrating schema");
761
762            let diagnostics = Diagnostics {
763                shard_name: global_id.to_string(),
764                handle_purpose: "evolve nullability for bootstrap".to_string(),
765            };
766            let evolve_result = persist_client
767                .compare_and_evolve_schema::<SourceData, (), Timestamp, StorageDiff>(
768                    shard_id,
769                    schema_id,
770                    &relation_desc,
771                    &UnitSchema,
772                    diagnostics,
773                )
774                .await
775                .expect("invalid persist usage");
776            match evolve_result {
777                CaESchema::Ok(_) => (),
778                CaESchema::ExpectedMismatch {
779                    schema_id,
780                    key,
781                    val: _,
782                } => {
783                    return Err(StorageError::PersistSchemaEvolveRace {
784                        global_id,
785                        shard_id,
786                        schema_id,
787                        relation_desc: key,
788                    });
789                }
790                CaESchema::Incompatible => {
791                    return Err(StorageError::PersistInvalidSchemaEvolve {
792                        global_id,
793                        shard_id,
794                    });
795                }
796            };
797        }
798
799        Ok(())
800    }
801
802    /// Create and "execute" the described collection.
803    ///
804    /// "Execute" is in scare quotes because what executing a collection means
805    /// varies widely based on the type of collection you're creating.
806    ///
807    /// The general process creating a collection undergoes is:
808    /// 1. Enrich the description we get from the user with the metadata only
809    ///    the storage controller's metadata. This is mostly a matter of
810    ///    separating concerns.
811    /// 2. Generate write and read persist handles for the collection.
812    /// 3. Store the collection's metadata in the appropriate field.
813    /// 4. "Execute" the collection. What that means is contingent on the type of
814    ///    collection. so consult the code for more details.
815    ///
816    // TODO(aljoscha): It would be swell if we could refactor this Leviathan of
817    // a method/move individual parts to their own methods. @guswynn observes
818    // that a number of these operations could be moved into fns on
819    // `DataSource`.
820    #[instrument(name = "storage::create_collections")]
821    async fn create_collections_for_bootstrap(
822        &mut self,
823        storage_metadata: &StorageMetadata,
824        register_ts: Option<Timestamp>,
825        mut collections: Vec<(GlobalId, CollectionDescription)>,
826        migrated_storage_collections: &BTreeSet<GlobalId>,
827    ) -> Result<(), StorageError> {
828        self.migrated_storage_collections
829            .extend(migrated_storage_collections.iter().cloned());
830
831        self.storage_collections
832            .create_collections_for_bootstrap(
833                storage_metadata,
834                register_ts,
835                collections.clone(),
836                migrated_storage_collections,
837            )
838            .await?;
839
840        // At this point we're connected to all the collection shards in persist. Our warming task
841        // is no longer useful, so abort it if it's still running.
842        drop(self.persist_warm_task.take());
843
844        // Validate first, to avoid corrupting state.
845        // 1. create a dropped identifier, or
846        // 2. create an existing identifier with a new description.
847        // Make sure to check for errors within `ingestions` as well.
848        collections.sort_by_key(|(id, _)| *id);
849        collections.dedup();
850        for pos in 1..collections.len() {
851            if collections[pos - 1].0 == collections[pos].0 {
852                return Err(StorageError::CollectionIdReused(collections[pos].0));
853            }
854        }
855
856        // We first enrich each collection description with some additional metadata...
857        let enriched_with_metadata = collections
858            .into_iter()
859            .map(|(id, description)| {
860                let data_shard = storage_metadata.get_collection_shard(id)?;
861
862                // If the shard is being managed by txn-wal (initially, tables), then we need to
863                // pass along the shard id for the txns shard to dataflow rendering.
864                let txns_shard = description
865                    .data_source
866                    .in_txns()
867                    .then(|| *self.txns_read.txns_id());
868
869                let metadata = CollectionMetadata {
870                    persist_location: self.persist_location.clone(),
871                    data_shard,
872                    relation_desc: description.desc.clone(),
873                    txns_shard,
874                };
875
876                Ok((id, description, metadata))
877            })
878            .collect_vec();
879
880        // So that we can open persist handles for each collections concurrently.
881        let persist_client = self
882            .persist
883            .open(self.persist_location.clone())
884            .await
885            .unwrap();
886        let persist_client = &persist_client;
887
888        // Reborrow the `&mut self` as immutable, as all the concurrent work to be processed in
889        // this stream cannot all have exclusive access.
890        use futures::stream::{StreamExt, TryStreamExt};
891        let this = &*self;
892        let mut to_register: Vec<_> = futures::stream::iter(enriched_with_metadata)
893            .map(|data: Result<_, StorageError>| {
894                async move {
895                    let (id, description, metadata) = data?;
896
897                    // should be replaced with real introspection (https://github.com/MaterializeInc/database-issues/issues/4078)
898                    // but for now, it's helpful to have this mapping written down somewhere
899                    debug!("mapping GlobalId={} to shard ({})", id, metadata.data_shard);
900
901                    // Tables are written through the txns table-write worker, which opens its
902                    // own write handles per registration, so opening one here would be pure
903                    // overhead (an extra persist open per table on the startup path). The
904                    // controller only needs a recent upper for them.
905                    let write = if matches!(description.data_source, DataSource::Table) {
906                        let diagnostics = Diagnostics {
907                            shard_name: id.to_string(),
908                            handle_purpose: format!("controller data for {}", id),
909                        };
910                        let upper = persist_client
911                            .recent_upper::<SourceData, (), Timestamp, StorageDiff>(
912                                metadata.data_shard,
913                                diagnostics,
914                            )
915                            .await
916                            .expect("invalid persist usage");
917                        WriteHandleOrUpper::Upper(upper)
918                    } else {
919                        let write = this
920                            .open_data_handles(
921                                &id,
922                                metadata.data_shard,
923                                metadata.relation_desc.clone(),
924                                persist_client,
925                            )
926                            .await;
927                        WriteHandleOrUpper::Handle(write)
928                    };
929
930                    Ok::<_, StorageError>((id, description, write, metadata))
931                }
932            })
933            // Poll each future for each collection concurrently, maximum of 50 at a time.
934            .buffer_unordered(50)
935            // HERE BE DRAGONS:
936            //
937            // There are at least 2 subtleties in using `FuturesUnordered` (which
938            // `buffer_unordered` uses underneath:
939            // - One is captured here <https://github.com/rust-lang/futures-rs/issues/2387>
940            // - And the other is deadlocking if processing an OUTPUT of a `FuturesUnordered`
941            // stream attempts to obtain an async mutex that is also obtained in the futures
942            // being polled.
943            //
944            // Both of these could potentially be issues in all usages of `buffer_unordered` in
945            // this method, so we stick the standard advice: only use `try_collect` or
946            // `collect`!
947            .try_collect()
948            .await?;
949
950        // The set of collections that we should render at the end of this
951        // function.
952        let mut to_execute = BTreeSet::new();
953        // New collections that are being created; this is distinct from the set
954        // of collections we plan to execute because
955        // `DataSource::IngestionExport` is added as a new collection, but is
956        // not executed directly.
957        let mut new_collections = BTreeSet::new();
958
959        // Reorder in dependency order.
960        to_register.sort_by_key(|(id, ..)| *id);
961
962        // Register tables first, but register them in reverse order since earlier tables
963        // can depend on later tables.
964        //
965        // Note: We could do more complex sorting to avoid the allocations, but IMO it's
966        // easier to reason about it this way.
967        let (tables_to_register, collections_to_register): (Vec<_>, Vec<_>) = to_register
968            .into_iter()
969            .partition(|(_id, desc, ..)| desc.data_source == DataSource::Table);
970        let to_register = tables_to_register
971            .into_iter()
972            .rev()
973            .chain(collections_to_register.into_iter());
974
975        // Statistics need a level of indirection so we can mutably borrow
976        // `self` when registering collections and when we are inserting
977        // statistics.
978        let mut new_webhook_statistic_entries = BTreeSet::new();
979
980        for (id, description, write, metadata) in to_register {
981            let is_in_txns = |id, metadata: &CollectionMetadata| {
982                metadata.txns_shard.is_some()
983                    && !(self.read_only && migrated_storage_collections.contains(&id))
984            };
985
986            to_execute.insert(id);
987            new_collections.insert(id);
988
989            let write_frontier = write.upper();
990
991            // Determine if this collection has another dependency.
992            let storage_dependencies = self.determine_collection_dependencies(id, &description)?;
993
994            let dependency_read_holds = self
995                .storage_collections
996                .acquire_read_holds(storage_dependencies)
997                .expect("can acquire read holds");
998
999            let mut dependency_since = Antichain::from_elem(Timestamp::MIN);
1000            for read_hold in dependency_read_holds.iter() {
1001                dependency_since.join_assign(read_hold.since());
1002            }
1003
1004            let data_source = description.data_source;
1005
1006            // Assert some invariants.
1007            //
1008            // TODO(alter_table): Include Tables (is_in_txns) in this check. After
1009            // supporting ALTER TABLE, it's now possible for a table to have a dependency
1010            // and thus run this check. But tables are managed by txn-wal and thus the
1011            // upper of the shard's `write_handle` generally isn't the logical upper of
1012            // the shard. Instead we need to thread through the upper of the `txn` shard
1013            // here so we can check this invariant.
1014            if !dependency_read_holds.is_empty()
1015                && !is_in_txns(id, &metadata)
1016                && !matches!(&data_source, DataSource::Sink { .. })
1017            {
1018                // As the halt message says, this can happen when trying to come
1019                // up in read-only mode and the current read-write
1020                // environmentd/controller deletes a collection. We exit
1021                // gracefully, which means we'll get restarted and get to try
1022                // again.
1023                //
1024                // We only do this when our own upper is not empty. An empty
1025                // upper means the collection is sealed and no-one can write to
1026                // it anymore, so an empty dependency since cannot hurt us. And
1027                // a sealed collection is durable state rather than a race, so
1028                // halting on it would wedge bootstrap forever instead of
1029                // resolving on the next attempt.
1030                if dependency_since.is_empty() && !write_frontier.is_empty() {
1031                    let reason = format!(
1032                        "dependency since frontier is empty while dependent upper \
1033                        is not empty (dependent id={id}, write_frontier={:?}, dependency_read_holds={:?}), \
1034                        this indicates concurrent deletion of a collection",
1035                        write_frontier, dependency_read_holds,
1036                    );
1037                    // A read-only controller can restart without Kubernetes
1038                    // crash backoff. If its generation has been promoted
1039                    // meanwhile, by a replacement pod or by this process after
1040                    // a skipped catch-up, `preflight_0dt` halts the restarted
1041                    // process.
1042                    if self.read_only {
1043                        restart_in_place!("{}", reason);
1044                    } else {
1045                        halt!("{}", reason);
1046                    }
1047                }
1048
1049                // If the dependency is between a remap shard and a source (and
1050                // not a "primary" dependency), the dependency since cannot be
1051                // beyond the dependent (our) upper unless the collection is
1052                // new. If the since is allowed to "catch up" to the upper, that
1053                // is `upper <= since`, a restarting ingestion cannot
1054                // differentiate between updates that have already been written
1055                // out to the backing persist shard and updates that have yet to
1056                // be written. We would write duplicate updates.
1057                //
1058                // If this check fails, it means that the read hold installed on
1059                // the dependency was probably not upheld –– if it were, the
1060                // dependency's since could not have advanced as far the
1061                // dependent's upper.
1062                //
1063                // We don't care about the dependency since when the write
1064                // frontier is empty. In that case, no-one can write down any
1065                // more updates.
1066                //
1067                // An ingestion export whose shard holds no updates is exempt,
1068                // see `StorageCollectionsImpl::create_collections_for_bootstrap`.
1069                if description.primary.is_none()
1070                    && write_frontier.elements() != &[Timestamp::MIN]
1071                    && !write_frontier.is_empty()
1072                    && !PartialOrder::less_than(&dependency_since, &write_frontier)
1073                    && !(matches!(data_source, DataSource::IngestionExport { .. })
1074                        && shard_has_no_updates(persist_client, id, &metadata).await)
1075                {
1076                    mz_ore::soft_panic_or_log!(
1077                        "dependency since has advanced past dependent ({id}) upper \n
1078                            dependent ({id}): upper {:?} \n
1079                            dependency since {:?} \n
1080                            dependency read holds: {:?}",
1081                        write_frontier,
1082                        dependency_since,
1083                        dependency_read_holds,
1084                    );
1085                }
1086            }
1087
1088            // Perform data source-specific setup.
1089            let mut extra_state = CollectionStateExtra::None;
1090            let mut maybe_instance_id = None;
1091            match &data_source {
1092                DataSource::Introspection(typ) => {
1093                    debug!(
1094                        ?data_source, meta = ?metadata,
1095                        "registering {id} with persist monotonic worker",
1096                    );
1097                    // We always register the collection with the collection manager,
1098                    // regardless of read-only mode. The CollectionManager itself is
1099                    // aware of read-only mode and will not attempt to write before told
1100                    // to do so.
1101                    //
1102                    self.register_introspection_collection(
1103                        id,
1104                        *typ,
1105                        write.expect_handle("introspection collections are not tables"),
1106                        persist_client.clone(),
1107                    )?;
1108                }
1109                DataSource::Webhook => {
1110                    debug!(
1111                        ?data_source, meta = ?metadata,
1112                        "registering {id} with persist monotonic worker",
1113                    );
1114                    // This collection of statistics is periodically aggregated into
1115                    // `source_statistics`.
1116                    new_webhook_statistic_entries.insert(id);
1117                    // Register the collection so our manager knows about it.
1118                    //
1119                    // NOTE: Maybe this shouldn't be in the collection manager,
1120                    // and collection manager should only be responsible for
1121                    // built-in introspection collections?
1122                    self.collection_manager.register_append_only_collection(
1123                        id,
1124                        write.expect_handle("webhook collections are not tables"),
1125                        false,
1126                        None,
1127                    );
1128                }
1129                DataSource::IngestionExport {
1130                    ingestion_id,
1131                    details,
1132                    data_config,
1133                } => {
1134                    debug!(
1135                        ?data_source, meta = ?metadata,
1136                        "not registering {id} with a controller persist worker",
1137                    );
1138                    // Adjust the source to contain this export.
1139                    let ingestion_state = self
1140                        .collections
1141                        .get_mut(ingestion_id)
1142                        .expect("known to exist");
1143
1144                    let instance_id = match &mut ingestion_state.data_source {
1145                        DataSource::Ingestion(ingestion_desc) => {
1146                            ingestion_desc.source_exports.insert(
1147                                id,
1148                                SourceExport {
1149                                    storage_metadata: (),
1150                                    details: details.clone(),
1151                                    data_config: data_config.clone(),
1152                                },
1153                            );
1154
1155                            // Record the ingestion's cluster ID for the
1156                            // ingestion export. This way we always have a
1157                            // record of it, even if the ingestion's collection
1158                            // description disappears.
1159                            ingestion_desc.instance_id
1160                        }
1161                        _ => unreachable!(
1162                            "SourceExport must only refer to primary sources that already exist"
1163                        ),
1164                    };
1165
1166                    // Executing the source export doesn't do anything, ensure we execute the source instead.
1167                    to_execute.remove(&id);
1168                    to_execute.insert(*ingestion_id);
1169
1170                    let ingestion_state = IngestionState {
1171                        read_capabilities: MutableAntichain::from(dependency_since.clone()),
1172                        dependency_read_holds,
1173                        derived_since: dependency_since,
1174                        write_frontier: Antichain::from_elem(Timestamp::MIN),
1175                        hold_policy: ReadPolicy::step_back(),
1176                        instance_id,
1177                        hydrated_on: BTreeSet::new(),
1178                    };
1179
1180                    extra_state = CollectionStateExtra::Ingestion(ingestion_state);
1181                    maybe_instance_id = Some(instance_id);
1182                }
1183                DataSource::Table => {
1184                    debug!(
1185                        ?data_source, meta = ?metadata,
1186                        "not registering {id} with the txns shard here; the caller does that \
1187                         through the group committer",
1188                    );
1189                }
1190                DataSource::Progress | DataSource::Other => {
1191                    debug!(
1192                        ?data_source, meta = ?metadata,
1193                        "not registering {id} with a controller persist worker",
1194                    );
1195                }
1196                DataSource::Ingestion(ingestion_desc) => {
1197                    debug!(
1198                        ?data_source, meta = ?metadata,
1199                        "not registering {id} with a controller persist worker",
1200                    );
1201
1202                    let mut dependency_since = Antichain::from_elem(Timestamp::MIN);
1203                    for read_hold in dependency_read_holds.iter() {
1204                        dependency_since.join_assign(read_hold.since());
1205                    }
1206
1207                    let ingestion_state = IngestionState {
1208                        read_capabilities: MutableAntichain::from(dependency_since.clone()),
1209                        dependency_read_holds,
1210                        derived_since: dependency_since,
1211                        write_frontier: Antichain::from_elem(Timestamp::MIN),
1212                        hold_policy: ReadPolicy::step_back(),
1213                        instance_id: ingestion_desc.instance_id,
1214                        hydrated_on: BTreeSet::new(),
1215                    };
1216
1217                    extra_state = CollectionStateExtra::Ingestion(ingestion_state);
1218                    maybe_instance_id = Some(ingestion_desc.instance_id);
1219                }
1220                DataSource::Sink { desc } => {
1221                    let mut dependency_since = Antichain::from_elem(Timestamp::MIN);
1222                    for read_hold in dependency_read_holds.iter() {
1223                        dependency_since.join_assign(read_hold.since());
1224                    }
1225
1226                    let [self_hold, read_hold] =
1227                        dependency_read_holds.try_into().expect("two holds");
1228
1229                    let state = ExportState::new(
1230                        desc.instance_id,
1231                        read_hold,
1232                        self_hold,
1233                        write_frontier.clone(),
1234                        ReadPolicy::step_back(),
1235                    );
1236                    maybe_instance_id = Some(state.cluster_id);
1237                    extra_state = CollectionStateExtra::Export(state);
1238                }
1239            }
1240
1241            let wallclock_lag_metrics = self.metrics.wallclock_lag_metrics(id, maybe_instance_id);
1242            let collection_state =
1243                CollectionState::new(data_source, metadata, extra_state, wallclock_lag_metrics);
1244
1245            self.collections.insert(id, collection_state);
1246        }
1247
1248        {
1249            let mut source_statistics = self.source_statistics.lock().expect("poisoned");
1250
1251            // Webhooks don't run on clusters/replicas, so we initialize their
1252            // statistics collection here.
1253            for id in new_webhook_statistic_entries {
1254                source_statistics.webhook_statistics.entry(id).or_default();
1255            }
1256
1257            // Sources and sinks only have statistics in the collection when
1258            // there is a replica that is reporting them. No need to initialize
1259            // here.
1260        }
1261
1262        self.append_shard_mappings(new_collections.into_iter(), Diff::ONE);
1263
1264        // TODO(guswynn): perform the io in this final section concurrently.
1265        for id in to_execute {
1266            match &self.collection(id)?.data_source {
1267                DataSource::Ingestion(ingestion) => {
1268                    if !self.read_only
1269                        || (ENABLE_0DT_DEPLOYMENT_SOURCES.get(self.config.config_set())
1270                            && ingestion.desc.connection.supports_read_only())
1271                    {
1272                        self.run_ingestion(id)?;
1273                    }
1274                }
1275                DataSource::IngestionExport { .. } => unreachable!(
1276                    "ingestion exports do not execute directly, but instead schedule their source to be re-executed"
1277                ),
1278                DataSource::Introspection(_)
1279                | DataSource::Webhook
1280                | DataSource::Table
1281                | DataSource::Progress
1282                | DataSource::Other => {}
1283                DataSource::Sink { .. } => {
1284                    if !self.read_only {
1285                        self.run_export(id)?;
1286                    }
1287                }
1288            };
1289        }
1290
1291        Ok(())
1292    }
1293
1294    fn check_alter_ingestion_source_desc(
1295        &mut self,
1296        ingestion_id: GlobalId,
1297        source_desc: &SourceDesc,
1298    ) -> Result<(), StorageError> {
1299        let source_collection = self.collection(ingestion_id)?;
1300        let data_source = &source_collection.data_source;
1301        match &data_source {
1302            DataSource::Ingestion(cur_ingestion) => {
1303                cur_ingestion
1304                    .desc
1305                    .alter_compatible(ingestion_id, source_desc)?;
1306            }
1307            o => {
1308                tracing::info!(
1309                    "{ingestion_id} inalterable because its data source is {:?} and not an ingestion",
1310                    o
1311                );
1312                Err(AlterError { id: ingestion_id })?
1313            }
1314        }
1315
1316        Ok(())
1317    }
1318
1319    async fn alter_ingestion_source_desc(
1320        &mut self,
1321        ingestion_ids: BTreeMap<GlobalId, SourceDesc>,
1322    ) -> Result<(), StorageError> {
1323        let mut ingestions_to_run = BTreeSet::new();
1324
1325        for (id, new_desc) in ingestion_ids {
1326            let collection = self
1327                .collections
1328                .get_mut(&id)
1329                .ok_or_else(|| StorageError::IdentifierMissing(id))?;
1330
1331            match &mut collection.data_source {
1332                DataSource::Ingestion(ingestion) => {
1333                    if ingestion.desc != new_desc {
1334                        tracing::info!(
1335                            from = ?ingestion.desc,
1336                            to = ?new_desc,
1337                            "alter_ingestion_source_desc, updating"
1338                        );
1339                        ingestion.desc = new_desc;
1340                        ingestions_to_run.insert(id);
1341                    }
1342                }
1343                o => {
1344                    tracing::warn!("alter_ingestion_source_desc called on {:?}", o);
1345                    Err(StorageError::IdentifierInvalid(id))?;
1346                }
1347            }
1348        }
1349
1350        for id in ingestions_to_run {
1351            self.run_ingestion(id)?;
1352        }
1353        Ok(())
1354    }
1355
1356    async fn alter_ingestion_connections(
1357        &mut self,
1358        source_connections: BTreeMap<GlobalId, GenericSourceConnection<InlinedConnection>>,
1359    ) -> Result<(), StorageError> {
1360        let mut ingestions_to_run = BTreeSet::new();
1361
1362        for (id, conn) in source_connections {
1363            let collection = self
1364                .collections
1365                .get_mut(&id)
1366                .ok_or_else(|| StorageError::IdentifierMissing(id))?;
1367
1368            match &mut collection.data_source {
1369                DataSource::Ingestion(ingestion) => {
1370                    // If the connection hasn't changed, there's no sense in
1371                    // re-rendering the dataflow.
1372                    if ingestion.desc.connection != conn {
1373                        tracing::info!(from = ?ingestion.desc.connection, to = ?conn, "alter_ingestion_connections, updating");
1374                        ingestion.desc.connection = conn;
1375                        ingestions_to_run.insert(id);
1376                    } else {
1377                        tracing::warn!(
1378                            "update_source_connection called on {id} but the \
1379                            connection was the same"
1380                        );
1381                    }
1382                }
1383                o => {
1384                    tracing::warn!("update_source_connection called on {:?}", o);
1385                    Err(StorageError::IdentifierInvalid(id))?;
1386                }
1387            }
1388        }
1389
1390        for id in ingestions_to_run {
1391            self.run_ingestion(id)?;
1392        }
1393        Ok(())
1394    }
1395
1396    async fn alter_ingestion_export_data_configs(
1397        &mut self,
1398        source_exports: BTreeMap<GlobalId, SourceExportDataConfig>,
1399    ) -> Result<(), StorageError> {
1400        let mut ingestions_to_run = BTreeSet::new();
1401
1402        for (source_export_id, new_data_config) in source_exports {
1403            // We need to adjust the data config on the CollectionState for
1404            // the source export collection directly
1405            let source_export_collection = self
1406                .collections
1407                .get_mut(&source_export_id)
1408                .ok_or_else(|| StorageError::IdentifierMissing(source_export_id))?;
1409            let ingestion_id = match &mut source_export_collection.data_source {
1410                DataSource::IngestionExport {
1411                    ingestion_id,
1412                    details: _,
1413                    data_config,
1414                } => {
1415                    *data_config = new_data_config.clone();
1416                    *ingestion_id
1417                }
1418                o => {
1419                    tracing::warn!("alter_ingestion_export_data_configs called on {:?}", o);
1420                    Err(StorageError::IdentifierInvalid(source_export_id))?
1421                }
1422            };
1423            // We also need to adjust the data config on the CollectionState of the
1424            // Ingestion that the export is associated with.
1425            let ingestion_collection = self
1426                .collections
1427                .get_mut(&ingestion_id)
1428                .ok_or_else(|| StorageError::IdentifierMissing(ingestion_id))?;
1429
1430            match &mut ingestion_collection.data_source {
1431                DataSource::Ingestion(ingestion_desc) => {
1432                    let source_export = ingestion_desc
1433                        .source_exports
1434                        .get_mut(&source_export_id)
1435                        .ok_or_else(|| StorageError::IdentifierMissing(source_export_id))?;
1436
1437                    // If the data config hasn't changed, there's no sense in
1438                    // re-rendering the dataflow.
1439                    if source_export.data_config != new_data_config {
1440                        tracing::info!(?source_export_id, from = ?source_export.data_config, to = ?new_data_config, "alter_ingestion_export_data_configs, updating");
1441                        source_export.data_config = new_data_config;
1442
1443                        ingestions_to_run.insert(ingestion_id);
1444                    } else {
1445                        tracing::warn!(
1446                            "alter_ingestion_export_data_configs called on \
1447                                    export {source_export_id} of {ingestion_id} but \
1448                                    the data config was the same"
1449                        );
1450                    }
1451                }
1452                o => {
1453                    tracing::warn!("alter_ingestion_export_data_configs called on {:?}", o);
1454                    Err(StorageError::IdentifierInvalid(ingestion_id))?
1455                }
1456            }
1457        }
1458
1459        for id in ingestions_to_run {
1460            self.run_ingestion(id)?;
1461        }
1462        Ok(())
1463    }
1464
1465    async fn alter_table_desc(
1466        &mut self,
1467        existing_collection: GlobalId,
1468        new_collection: GlobalId,
1469        new_desc: RelationDesc,
1470        expected_version: RelationVersion,
1471    ) -> Result<(), StorageError> {
1472        let data_shard = {
1473            let Controller {
1474                collections,
1475                storage_collections,
1476                ..
1477            } = self;
1478
1479            let existing = collections
1480                .get(&existing_collection)
1481                .ok_or(StorageError::IdentifierMissing(existing_collection))?;
1482            if existing.data_source != DataSource::Table {
1483                return Err(StorageError::IdentifierInvalid(existing_collection));
1484            }
1485
1486            // Let StorageCollections know!
1487            storage_collections
1488                .alter_table_desc(
1489                    existing_collection,
1490                    new_collection,
1491                    new_desc.clone(),
1492                    expected_version,
1493                )
1494                .await?;
1495
1496            existing.collection_metadata.data_shard.clone()
1497        };
1498
1499        let collection_meta = CollectionMetadata {
1500            persist_location: self.persist_location.clone(),
1501            data_shard,
1502            relation_desc: new_desc.clone(),
1503            // TODO(alter_table): Support schema evolution on sources.
1504            txns_shard: Some(self.txns_read.txns_id().clone()),
1505        };
1506        // TODO(alter_table): Support schema evolution on sources.
1507        let wallclock_lag_metrics = self.metrics.wallclock_lag_metrics(new_collection, None);
1508        let collection_state = CollectionState::new(
1509            DataSource::Table,
1510            collection_meta,
1511            CollectionStateExtra::None,
1512            wallclock_lag_metrics,
1513        );
1514
1515        // Great! We have successfully evolved the schema of our Table, now we need to update our
1516        // in-memory data structures.
1517        self.collections.insert(new_collection, collection_state);
1518
1519        self.append_shard_mappings([new_collection].into_iter(), Diff::ONE);
1520
1521        Ok(())
1522    }
1523
1524    async fn register_table_collections(
1525        &mut self,
1526        register_ts: Timestamp,
1527        ids: Vec<GlobalId>,
1528    ) -> Result<(), StorageError> {
1529        let mut tables = self.table_registrations(ids)?;
1530
1531        // A read-only deployment only writes its migrated builtin tables.
1532        if self.read_only {
1533            tables.retain(|table| self.migrated_storage_collections.contains(&table.id));
1534        }
1535        if tables.is_empty() {
1536            return Ok(());
1537        }
1538
1539        match self
1540            .persist_table_worker
1541            .register(register_ts, tables)
1542            .await
1543        {
1544            Ok(res) => res,
1545            Err(_recv) => Err(StorageError::ShuttingDown("persist_table_worker")),
1546        }
1547    }
1548
1549    fn table_registrations(
1550        &self,
1551        ids: Vec<GlobalId>,
1552    ) -> Result<Vec<TableRegistration>, StorageError> {
1553        // The storage data source decides which table catalog items use txn-wal.
1554        let mut tables = Vec::with_capacity(ids.len());
1555        for id in ids {
1556            let collection = self.collection(id)?;
1557            if matches!(collection.data_source, DataSource::Table) {
1558                let metadata = &collection.collection_metadata;
1559                tables.push(TableRegistration {
1560                    id,
1561                    data_shard: metadata.data_shard,
1562                    relation_desc: metadata.relation_desc.clone(),
1563                });
1564            }
1565        }
1566        Ok(tables)
1567    }
1568
1569    fn txns_table_ids(&self, ids: Vec<GlobalId>) -> Result<Vec<GlobalId>, StorageError> {
1570        let mut tables = Vec::with_capacity(ids.len());
1571        for id in ids {
1572            let collection = self.collection(id)?;
1573            if matches!(collection.data_source, DataSource::Table) {
1574                tables.push(id);
1575            }
1576        }
1577        Ok(tables)
1578    }
1579
1580    fn export(&self, id: GlobalId) -> Result<&ExportState, StorageError> {
1581        self.collections
1582            .get(&id)
1583            .and_then(|c| match &c.extra_state {
1584                CollectionStateExtra::Export(state) => Some(state),
1585                _ => None,
1586            })
1587            .ok_or(StorageError::IdentifierMissing(id))
1588    }
1589
1590    fn export_mut(&mut self, id: GlobalId) -> Result<&mut ExportState, StorageError> {
1591        self.collections
1592            .get_mut(&id)
1593            .and_then(|c| match &mut c.extra_state {
1594                CollectionStateExtra::Export(state) => Some(state),
1595                _ => None,
1596            })
1597            .ok_or(StorageError::IdentifierMissing(id))
1598    }
1599
1600    /// Create a oneshot ingestion.
1601    async fn create_oneshot_ingestion(
1602        &mut self,
1603        ingestion_id: uuid::Uuid,
1604        collection_id: GlobalId,
1605        instance_id: StorageInstanceId,
1606        request: OneshotIngestionRequest,
1607        result_tx: OneshotResultCallback<ProtoBatch>,
1608    ) -> Result<(), StorageError> {
1609        let collection_meta = self
1610            .collections
1611            .get(&collection_id)
1612            .ok_or_else(|| StorageError::IdentifierMissing(collection_id))?
1613            .collection_metadata
1614            .clone();
1615        let instance = self.instances.get_mut(&instance_id).ok_or_else(|| {
1616            // TODO(cf2): Refine this error.
1617            StorageError::Generic(anyhow::anyhow!("missing cluster {instance_id}"))
1618        })?;
1619        let oneshot_cmd = RunOneshotIngestion {
1620            ingestion_id,
1621            collection_id,
1622            collection_meta,
1623            request,
1624        };
1625
1626        if !self.read_only {
1627            instance.send(StorageCommand::RunOneshotIngestion(Box::new(oneshot_cmd)));
1628            let pending = PendingOneshotIngestion {
1629                result_tx,
1630                cluster_id: instance_id,
1631            };
1632            let novel = self
1633                .pending_oneshot_ingestions
1634                .insert(ingestion_id, pending);
1635            assert_none!(novel);
1636            Ok(())
1637        } else {
1638            Err(StorageError::ReadOnly)
1639        }
1640    }
1641
1642    fn cancel_oneshot_ingestion(&mut self, ingestion_id: uuid::Uuid) -> Result<(), StorageError> {
1643        if self.read_only {
1644            return Err(StorageError::ReadOnly);
1645        }
1646
1647        let pending = self
1648            .pending_oneshot_ingestions
1649            .remove(&ingestion_id)
1650            .ok_or_else(|| {
1651                // TODO(cf2): Refine this error.
1652                StorageError::Generic(anyhow::anyhow!("missing oneshot ingestion {ingestion_id}"))
1653            })?;
1654
1655        match self.instances.get_mut(&pending.cluster_id) {
1656            Some(instance) => {
1657                instance.send(StorageCommand::CancelOneshotIngestion(ingestion_id));
1658            }
1659            None => {
1660                mz_ore::soft_panic_or_log!(
1661                    "canceling oneshot ingestion on non-existent cluster, ingestion {:?}, instance {}",
1662                    ingestion_id,
1663                    pending.cluster_id,
1664                );
1665            }
1666        }
1667        // Respond to the user that the request has been canceled.
1668        pending.cancel();
1669
1670        Ok(())
1671    }
1672
1673    async fn alter_export(
1674        &mut self,
1675        id: GlobalId,
1676        new_description: ExportDescription,
1677    ) -> Result<(), StorageError> {
1678        let from_id = new_description.sink.from;
1679
1680        // Acquire read holds at StorageCollections to ensure that the
1681        // sinked collection is not dropped while we're sinking it.
1682        let desired_read_holds = vec![from_id.clone(), id.clone()];
1683        let [input_hold, self_hold] = self
1684            .storage_collections
1685            .acquire_read_holds(desired_read_holds)
1686            .expect("missing dependency")
1687            .try_into()
1688            .expect("expected number of holds");
1689        let from_storage_metadata = self.storage_collections.collection_metadata(from_id)?;
1690        let to_storage_metadata = self.storage_collections.collection_metadata(id)?;
1691
1692        // Check whether the sink's write frontier is beyond the read hold we got
1693        let cur_export = self.export_mut(id)?;
1694        let input_readable = cur_export
1695            .write_frontier
1696            .iter()
1697            .all(|t| input_hold.since().less_than(t));
1698        if !input_readable {
1699            return Err(StorageError::ReadBeforeSince(from_id));
1700        }
1701
1702        let new_export = ExportState {
1703            read_capabilities: cur_export.read_capabilities.clone(),
1704            cluster_id: new_description.instance_id,
1705            derived_since: cur_export.derived_since.clone(),
1706            read_holds: [input_hold, self_hold],
1707            read_policy: cur_export.read_policy.clone(),
1708            write_frontier: cur_export.write_frontier.clone(),
1709        };
1710        *cur_export = new_export;
1711
1712        // For `ALTER SINK`, the snapshot should only occur if the sink has not made any progress.
1713        // This prevents unnecessary decoding in the sink.
1714        // If the write frontier of the sink is strictly larger than its read hold, it must have at
1715        // least written out its snapshot, and we can skip reading it; otherwise assume we may have
1716        // to replay from the beginning.
1717        // TODO(SS-513): unify this with run_export, if possible
1718        let with_snapshot = new_description.sink.with_snapshot
1719            && !PartialOrder::less_than(&new_description.sink.as_of, &cur_export.write_frontier);
1720
1721        let cmd = RunSinkCommand {
1722            id,
1723            description: StorageSinkDesc {
1724                from: from_id,
1725                from_desc: new_description.sink.from_desc.clone(),
1726                connection: new_description.sink.connection.clone(),
1727                envelope: new_description.sink.envelope,
1728                as_of: new_description.sink.as_of.clone(),
1729                version: new_description.sink.version,
1730                from_storage_metadata,
1731                with_snapshot,
1732                to_storage_metadata,
1733                commit_interval: new_description.sink.commit_interval,
1734            },
1735        };
1736
1737        // Fetch the client for this export's cluster.
1738        let instance = self
1739            .instances
1740            .get_mut(&new_description.instance_id)
1741            .ok_or_else(|| StorageError::ExportInstanceMissing {
1742                storage_instance_id: new_description.instance_id,
1743                export_id: id,
1744            })?;
1745
1746        instance.send(StorageCommand::RunSink(Box::new(cmd)));
1747
1748        // alter_export_connections rebuilds the sink from this description and
1749        // checks new connections against it with alter_compatible, so it must
1750        // describe the sink we just told the cluster to run.
1751        let state = self
1752            .collections
1753            .get_mut(&id)
1754            .expect("export known to exist");
1755        let DataSource::Sink { desc } = &mut state.data_source else {
1756            panic!("export known to exist")
1757        };
1758        *desc = new_description;
1759
1760        Ok(())
1761    }
1762
1763    /// Create the sinks described by the `ExportDescription`.
1764    async fn alter_export_connections(
1765        &mut self,
1766        exports: BTreeMap<GlobalId, StorageSinkConnection>,
1767    ) -> Result<(), StorageError> {
1768        let mut updates_by_instance =
1769            BTreeMap::<StorageInstanceId, Vec<(RunSinkCommand, ExportDescription)>>::new();
1770
1771        for (id, connection) in exports {
1772            // We stage changes in new_export_description and then apply all
1773            // updates to exports at the end.
1774            //
1775            // We don't just go ahead and clone the `ExportState` itself and
1776            // update that because `ExportState` is not clone, because it holds
1777            // a `ReadHandle` and cloning that would cause additional work for
1778            // whoever guarantees those read holds.
1779            let (current_export_description, as_of): (ExportDescription, _) = {
1780                let export = &self.collections[&id];
1781                let DataSource::Sink { desc } = &export.data_source else {
1782                    panic!("export exists")
1783                };
1784                let CollectionStateExtra::Export(state) = &export.extra_state else {
1785                    panic!("export exists")
1786                };
1787                let export_description = desc.clone();
1788                let as_of = state.input_hold().since().clone();
1789
1790                (export_description, as_of)
1791            };
1792
1793            let mut new_export_description = current_export_description.clone();
1794
1795            new_export_description.sink.connection = connection;
1796
1797            // Ensure compatibility
1798            current_export_description
1799                .sink
1800                .alter_compatible(id, &new_export_description.sink)?;
1801
1802            let from_storage_metadata = self
1803                .storage_collections
1804                .collection_metadata(new_export_description.sink.from)?;
1805            let to_storage_metadata = self.storage_collections.collection_metadata(id)?;
1806
1807            let cmd = RunSinkCommand {
1808                id,
1809                description: StorageSinkDesc {
1810                    from: new_export_description.sink.from,
1811                    from_desc: new_export_description.sink.from_desc.clone(),
1812                    connection: new_export_description.sink.connection.clone(),
1813                    envelope: new_export_description.sink.envelope,
1814                    with_snapshot: new_export_description.sink.with_snapshot,
1815                    version: new_export_description.sink.version,
1816                    // Here we are about to send a RunSinkCommand with the current read capaibility
1817                    // held by this sink. However, clusters are already running a version of the
1818                    // sink and nothing guarantees that by the time this command arrives at the
1819                    // clusters they won't have made additional progress such that this read
1820                    // capability is invalidated.
1821                    // The solution to this problem is for the controller to track specific
1822                    // executions of dataflows such that it can track the shutdown of the current
1823                    // instance and the initialization of the new instance separately and ensure
1824                    // read holds are held for the correct amount of time.
1825                    // TODO(petrosagg): change the controller to explicitly track dataflow executions
1826                    as_of: as_of.to_owned(),
1827                    from_storage_metadata,
1828                    to_storage_metadata,
1829                    commit_interval: new_export_description.sink.commit_interval,
1830                },
1831            };
1832
1833            let update = updates_by_instance
1834                .entry(new_export_description.instance_id)
1835                .or_default();
1836            update.push((cmd, new_export_description));
1837        }
1838
1839        for (instance_id, updates) in updates_by_instance {
1840            let mut export_updates = BTreeMap::new();
1841            let mut cmds = Vec::with_capacity(updates.len());
1842
1843            for (cmd, export_state) in updates {
1844                export_updates.insert(cmd.id, export_state);
1845                cmds.push(cmd);
1846            }
1847
1848            // Fetch the client for this exports's cluster.
1849            let instance = self.instances.get_mut(&instance_id).ok_or_else(|| {
1850                StorageError::ExportInstanceMissing {
1851                    storage_instance_id: instance_id,
1852                    export_id: *export_updates
1853                        .keys()
1854                        .next()
1855                        .expect("set of exports not empty"),
1856                }
1857            })?;
1858
1859            for cmd in cmds {
1860                instance.send(StorageCommand::RunSink(Box::new(cmd)));
1861            }
1862
1863            // Update state only after all possible errors have occurred.
1864            for (id, new_export_description) in export_updates {
1865                let Some(state) = self.collections.get_mut(&id) else {
1866                    panic!("export known to exist")
1867                };
1868                let DataSource::Sink { desc } = &mut state.data_source else {
1869                    panic!("export known to exist")
1870                };
1871                *desc = new_export_description;
1872            }
1873        }
1874
1875        Ok(())
1876    }
1877
1878    fn drop_tables(
1879        &mut self,
1880        storage_metadata: &StorageMetadata,
1881        identifiers: Vec<GlobalId>,
1882    ) -> Result<(), StorageError> {
1883        let (table_write_ids, data_source_ids): (Vec<_>, Vec<_>) = identifiers
1884            .into_iter()
1885            .partition(|id| match self.collections[id].data_source {
1886                DataSource::Table => true,
1887                DataSource::IngestionExport { .. } | DataSource::Webhook => false,
1888                _ => panic!("identifier is not a table: {}", id),
1889            });
1890
1891        if table_write_ids.len() > 0 {
1892            let tx = self.pending_table_handle_drops_tx.clone();
1893            for identifier in table_write_ids {
1894                let _ = tx.send(identifier);
1895            }
1896        }
1897
1898        if data_source_ids.len() > 0 {
1899            self.validate_collection_ids(data_source_ids.iter().cloned())?;
1900            self.drop_sources_unvalidated(storage_metadata, data_source_ids)?;
1901        }
1902
1903        Ok(())
1904    }
1905
1906    fn drop_sources(
1907        &mut self,
1908        storage_metadata: &StorageMetadata,
1909        identifiers: Vec<GlobalId>,
1910    ) -> Result<(), StorageError> {
1911        self.validate_collection_ids(identifiers.iter().cloned())?;
1912        self.drop_sources_unvalidated(storage_metadata, identifiers)
1913    }
1914
1915    fn drop_sources_unvalidated(
1916        &mut self,
1917        storage_metadata: &StorageMetadata,
1918        ids: Vec<GlobalId>,
1919    ) -> Result<(), StorageError> {
1920        // Keep track of which ingestions we have to execute still, and which we
1921        // have to change because of dropped subsources.
1922        let mut ingestions_to_execute = BTreeSet::new();
1923        let mut ingestions_to_drop = BTreeSet::new();
1924        let mut source_statistics_to_drop = Vec::new();
1925
1926        // Ingestions (and their exports) are also collections, but we keep
1927        // track of non-ingestion collections separately, because they have
1928        // slightly different cleanup logic below.
1929        let mut collections_to_drop = Vec::new();
1930
1931        for id in ids.iter() {
1932            let collection_state = self.collections.get(id);
1933
1934            if let Some(collection_state) = collection_state {
1935                match collection_state.data_source {
1936                    DataSource::Webhook => {
1937                        // TODO(parkmycar): The Collection Manager and PersistMonotonicWriter
1938                        // could probably use some love and maybe get merged together?
1939                        let fut = self.collection_manager.unregister_collection(*id);
1940                        mz_ore::task::spawn(|| format!("storage-webhook-cleanup-{id}"), fut);
1941
1942                        collections_to_drop.push(*id);
1943                        source_statistics_to_drop.push(*id);
1944                    }
1945                    DataSource::Ingestion(_) => {
1946                        ingestions_to_drop.insert(*id);
1947                        source_statistics_to_drop.push(*id);
1948                    }
1949                    DataSource::IngestionExport { ingestion_id, .. } => {
1950                        // If we are dropping source exports, we need to modify the
1951                        // ingestion that it runs on.
1952                        //
1953                        // If we remove this export, we need to stop producing data to
1954                        // it, so plan to re-execute the ingestion with the amended
1955                        // description.
1956                        ingestions_to_execute.insert(ingestion_id);
1957
1958                        // Adjust the source to remove this export.
1959                        let ingestion_state = match self.collections.get_mut(&ingestion_id) {
1960                            Some(ingestion_collection) => ingestion_collection,
1961                            // Primary ingestion already dropped.
1962                            None => {
1963                                tracing::error!(
1964                                    "primary source {ingestion_id} seemingly dropped before subsource {id}"
1965                                );
1966                                continue;
1967                            }
1968                        };
1969
1970                        match &mut ingestion_state.data_source {
1971                            DataSource::Ingestion(ingestion_desc) => {
1972                                let removed = ingestion_desc.source_exports.remove(id);
1973                                mz_ore::soft_assert_or_log!(
1974                                    removed.is_some(),
1975                                    "dropped subsource {id} already removed from source exports"
1976                                );
1977                            }
1978                            _ => unreachable!(
1979                                "SourceExport must only refer to primary sources that already exist"
1980                            ),
1981                        };
1982
1983                        // Ingestion exports also have ReadHolds that we need to
1984                        // downgrade, and much of their drop machinery is the
1985                        // same as for the "main" ingestion.
1986                        ingestions_to_drop.insert(*id);
1987                        source_statistics_to_drop.push(*id);
1988                    }
1989                    DataSource::Progress | DataSource::Table | DataSource::Other => {
1990                        collections_to_drop.push(*id);
1991                    }
1992                    DataSource::Introspection(_) | DataSource::Sink { .. } => {
1993                        // Collections of these types are either not sources and should be dropped
1994                        // through other means, or are sources but should never be dropped.
1995                        soft_panic_or_log!(
1996                            "drop_sources called on a {:?} (id={id}))",
1997                            collection_state.data_source,
1998                        );
1999                    }
2000                }
2001            }
2002        }
2003
2004        // Do not bother re-executing ingestions we know we plan to drop.
2005        ingestions_to_execute.retain(|id| !ingestions_to_drop.contains(id));
2006        for ingestion_id in ingestions_to_execute {
2007            self.run_ingestion(ingestion_id)?;
2008        }
2009
2010        // For ingestions, we fabricate a new hold that will propagate through
2011        // the cluster and then back to us.
2012
2013        // We don't explicitly remove read capabilities! Downgrading the
2014        // frontier of the source to `[]` (the empty Antichain), will propagate
2015        // to the storage dependencies.
2016        let ingestion_policies = ingestions_to_drop
2017            .iter()
2018            .map(|id| (*id, ReadPolicy::ValidFrom(Antichain::new())))
2019            .collect();
2020
2021        tracing::debug!(
2022            ?ingestion_policies,
2023            "dropping sources by setting read hold policies"
2024        );
2025        self.set_hold_policies(ingestion_policies);
2026
2027        // Delete all collection->shard mappings
2028        let shards_to_update: BTreeSet<_> = ingestions_to_drop
2029            .iter()
2030            .chain(collections_to_drop.iter())
2031            .cloned()
2032            .collect();
2033        self.append_shard_mappings(shards_to_update.into_iter(), Diff::MINUS_ONE);
2034
2035        let status_now = mz_ore::now::to_datetime((self.now)());
2036        let mut status_updates = vec![];
2037        for id in ingestions_to_drop.iter() {
2038            status_updates.push(StatusUpdate::new(*id, status_now, Status::Dropped));
2039        }
2040
2041        if !self.read_only {
2042            self.append_status_introspection_updates(
2043                IntrospectionType::SourceStatusHistory,
2044                status_updates,
2045            );
2046        }
2047
2048        {
2049            let mut source_statistics = self.source_statistics.lock().expect("poisoned");
2050            for id in source_statistics_to_drop {
2051                source_statistics
2052                    .source_statistics
2053                    .retain(|(stats_id, _), _| stats_id != &id);
2054                source_statistics
2055                    .webhook_statistics
2056                    .retain(|stats_id, _| stats_id != &id);
2057            }
2058        }
2059
2060        // Remove collection state
2061        for id in ingestions_to_drop.iter().chain(collections_to_drop.iter()) {
2062            tracing::info!(%id, "dropping collection state");
2063            let collection = self
2064                .collections
2065                .remove(id)
2066                .expect("list populated after checking that self.collections contains it");
2067
2068            let instance = match &collection.extra_state {
2069                CollectionStateExtra::Ingestion(ingestion) => Some(ingestion.instance_id),
2070                CollectionStateExtra::Export(export) => Some(export.cluster_id()),
2071                CollectionStateExtra::None => None,
2072            }
2073            .and_then(|i| self.instances.get(&i));
2074
2075            // Record which replicas were running a collection, so that we can
2076            // match DroppedId messages against them and eventually remove state
2077            // from self.dropped_objects
2078            if let Some(instance) = instance {
2079                let active_replicas = instance.get_active_replicas_for_object(id);
2080                if !active_replicas.is_empty() {
2081                    // The remap collection of an ingestion doesn't have extra
2082                    // state and doesn't have an instance_id, but we still get
2083                    // upper updates for them, so want to make sure to populate
2084                    // dropped_ids.
2085                    // TODO(aljoscha): All this is a bit icky. But here we are
2086                    // for now...
2087                    match &collection.data_source {
2088                        DataSource::Ingestion(ingestion_desc) => {
2089                            if *id != ingestion_desc.remap_collection_id {
2090                                self.dropped_objects.insert(
2091                                    ingestion_desc.remap_collection_id,
2092                                    active_replicas.clone(),
2093                                );
2094                            }
2095                        }
2096                        _ => {}
2097                    }
2098
2099                    self.dropped_objects.insert(*id, active_replicas);
2100                }
2101            }
2102        }
2103
2104        // Also let StorageCollections know!
2105        self.storage_collections
2106            .drop_collections_unvalidated(storage_metadata, ids);
2107
2108        Ok(())
2109    }
2110
2111    /// Drops the read capability for the sinks and allows their resources to be reclaimed.
2112    fn drop_sinks(
2113        &mut self,
2114        storage_metadata: &StorageMetadata,
2115        identifiers: Vec<GlobalId>,
2116    ) -> Result<(), StorageError> {
2117        self.validate_export_ids(identifiers.iter().cloned())?;
2118        self.drop_sinks_unvalidated(storage_metadata, identifiers);
2119        Ok(())
2120    }
2121
2122    fn drop_sinks_unvalidated(
2123        &mut self,
2124        storage_metadata: &StorageMetadata,
2125        mut sinks_to_drop: Vec<GlobalId>,
2126    ) {
2127        // Ignore exports that have already been removed.
2128        sinks_to_drop.retain(|id| self.export(*id).is_ok());
2129
2130        // TODO: ideally we'd advance the write frontier ourselves here, but this function's
2131        // not yet marked async.
2132
2133        // We don't explicitly remove read capabilities! Downgrading the
2134        // frontier of the source to `[]` (the empty Antichain), will propagate
2135        // to the storage dependencies.
2136        let drop_policy = sinks_to_drop
2137            .iter()
2138            .map(|id| (*id, ReadPolicy::ValidFrom(Antichain::new())))
2139            .collect();
2140
2141        tracing::debug!(
2142            ?drop_policy,
2143            "dropping sources by setting read hold policies"
2144        );
2145        self.set_hold_policies(drop_policy);
2146
2147        // Record the drop status for all sink drops.
2148        //
2149        // We also delete the items' statistics objects.
2150        //
2151        // The locks are held for a short time, only while we do some removals from a map.
2152
2153        let status_now = mz_ore::now::to_datetime((self.now)());
2154
2155        // Record the drop status for all pending sink drops.
2156        let mut status_updates = vec![];
2157        {
2158            let mut sink_statistics = self.sink_statistics.lock().expect("poisoned");
2159            for id in sinks_to_drop.iter() {
2160                status_updates.push(StatusUpdate::new(*id, status_now, Status::Dropped));
2161                sink_statistics.retain(|(stats_id, _), _| stats_id != id);
2162            }
2163        }
2164
2165        if !self.read_only {
2166            self.append_status_introspection_updates(
2167                IntrospectionType::SinkStatusHistory,
2168                status_updates,
2169            );
2170        }
2171
2172        // Remove collection/export state
2173        for id in sinks_to_drop.iter() {
2174            tracing::info!(%id, "dropping export state");
2175            let collection = self
2176                .collections
2177                .remove(id)
2178                .expect("list populated after checking that self.collections contains it");
2179
2180            let instance = match &collection.extra_state {
2181                CollectionStateExtra::Ingestion(ingestion) => Some(ingestion.instance_id),
2182                CollectionStateExtra::Export(export) => Some(export.cluster_id()),
2183                CollectionStateExtra::None => None,
2184            }
2185            .and_then(|i| self.instances.get(&i));
2186
2187            // Record how many replicas were running an export, so that we can
2188            // match `DroppedId` messages against it and eventually remove state
2189            // from `self.dropped_objects`.
2190            if let Some(instance) = instance {
2191                let active_replicas = instance.get_active_replicas_for_object(id);
2192                if !active_replicas.is_empty() {
2193                    self.dropped_objects.insert(*id, active_replicas);
2194                }
2195            }
2196        }
2197
2198        // Also let StorageCollections know!
2199        self.storage_collections
2200            .drop_collections_unvalidated(storage_metadata, sinks_to_drop);
2201    }
2202
2203    #[instrument(level = "debug")]
2204    fn append_table(
2205        &mut self,
2206        write_ts: Timestamp,
2207        advance_to: Timestamp,
2208        commands: Vec<(GlobalId, Vec<TableData>)>,
2209    ) -> Result<tokio::sync::oneshot::Receiver<Result<(), StorageError>>, StorageError> {
2210        if self.read_only {
2211            // While in read only mode, ONLY collections that have been migrated
2212            // and need to be re-hydrated in read only mode can be written to.
2213            if !commands
2214                .iter()
2215                .all(|(id, _)| id.is_system() && self.migrated_storage_collections.contains(id))
2216            {
2217                return Err(StorageError::ReadOnly);
2218            }
2219        }
2220
2221        // TODO(petrosagg): validate appends against the expected RelationDesc of the collection
2222        for (id, updates) in commands.iter() {
2223            if !updates.is_empty() {
2224                if !write_ts.less_than(&advance_to) {
2225                    return Err(StorageError::UpdateBeyondUpper(*id));
2226                }
2227            }
2228        }
2229
2230        Ok(self
2231            .persist_table_worker
2232            .append(write_ts, advance_to, commands))
2233    }
2234
2235    fn table_write_handle(&self) -> Arc<dyn mz_storage_client::controller::TableWriteHandle> {
2236        Arc::new(persist_handles::TableWriteWorkerHandle(
2237            self.persist_table_worker.clone(),
2238        ))
2239    }
2240
2241    fn monotonic_appender(&self, id: GlobalId) -> Result<MonotonicAppender, StorageError> {
2242        self.collection_manager.monotonic_appender(id)
2243    }
2244
2245    fn webhook_statistics(&self, id: GlobalId) -> Result<Arc<WebhookStatistics>, StorageError> {
2246        // Call to this method are usually cached so the lock is not in the critical path.
2247        let source_statistics = self.source_statistics.lock().expect("poisoned");
2248        source_statistics
2249            .webhook_statistics
2250            .get(&id)
2251            .cloned()
2252            .ok_or(StorageError::IdentifierMissing(id))
2253    }
2254
2255    async fn ready(&mut self) {
2256        if self.maintenance_scheduled {
2257            return;
2258        }
2259
2260        if !self.pending_table_handle_drops_rx.is_empty() {
2261            return;
2262        }
2263
2264        tokio::select! {
2265            Some(m) = self.instance_response_rx.recv() => {
2266                self.stashed_responses.push(m);
2267                while let Ok(m) = self.instance_response_rx.try_recv() {
2268                    self.stashed_responses.push(m);
2269                }
2270            }
2271            _ = self.maintenance_ticker.tick() => {
2272                self.maintenance_scheduled = true;
2273            },
2274        };
2275    }
2276
2277    #[instrument(level = "debug")]
2278    fn process(
2279        &mut self,
2280        storage_metadata: &StorageMetadata,
2281    ) -> Result<Option<Response>, anyhow::Error> {
2282        // Perform periodic maintenance work.
2283        if self.maintenance_scheduled {
2284            self.maintain();
2285            self.maintenance_scheduled = false;
2286        }
2287
2288        for instance in self.instances.values_mut() {
2289            instance.rehydrate_failed_replicas();
2290        }
2291
2292        let mut status_updates = vec![];
2293        let mut updated_frontiers = BTreeMap::new();
2294
2295        // Take the currently stashed responses so that we can call mut receiver functions in the loop.
2296        let stashed_responses = std::mem::take(&mut self.stashed_responses);
2297        for resp in stashed_responses {
2298            match resp {
2299                (_replica_id, StorageResponse::FrontierUpper(id, upper)) => {
2300                    self.update_write_frontier(id, &upper);
2301                    updated_frontiers.insert(id, upper);
2302                }
2303                (replica_id, StorageResponse::DroppedId(id)) => {
2304                    let replica_id = replica_id.expect("DroppedId from unknown replica");
2305                    if let Some(remaining_replicas) = self.dropped_objects.get_mut(&id) {
2306                        remaining_replicas.remove(&replica_id);
2307                        if remaining_replicas.is_empty() {
2308                            self.dropped_objects.remove(&id);
2309                        }
2310                    } else {
2311                        soft_panic_or_log!("unexpected DroppedId for {id}");
2312                    }
2313                }
2314                (replica_id, StorageResponse::StatisticsUpdates(source_stats, sink_stats)) => {
2315                    // Note we only hold the locks while moving some plain-old-data around here.
2316                    {
2317                        // NOTE(aljoscha): We explicitly unwrap the `Option`,
2318                        // because we expect that stats coming from replicas
2319                        // have a replica id.
2320                        //
2321                        // If this is `None` and we would use that to access
2322                        // state below we might clobber something unexpectedly.
2323                        let replica_id = if let Some(replica_id) = replica_id {
2324                            replica_id
2325                        } else {
2326                            tracing::error!(
2327                                ?source_stats,
2328                                "missing replica_id for source statistics update"
2329                            );
2330                            continue;
2331                        };
2332
2333                        let mut shared_stats = self.source_statistics.lock().expect("poisoned");
2334
2335                        for stat in source_stats {
2336                            let collection_id = stat.id.clone();
2337
2338                            if self.collection(collection_id).is_err() {
2339                                // We can get updates for collections that have
2340                                // already been deleted, ignore those.
2341                                continue;
2342                            }
2343
2344                            let entry = shared_stats
2345                                .source_statistics
2346                                .entry((stat.id, Some(replica_id)));
2347
2348                            match entry {
2349                                btree_map::Entry::Vacant(vacant_entry) => {
2350                                    let mut stats = ControllerSourceStatistics::new(
2351                                        collection_id,
2352                                        Some(replica_id),
2353                                    );
2354                                    stats.incorporate(stat);
2355                                    vacant_entry.insert(stats);
2356                                }
2357                                btree_map::Entry::Occupied(mut occupied_entry) => {
2358                                    occupied_entry.get_mut().incorporate(stat);
2359                                }
2360                            }
2361                        }
2362                    }
2363
2364                    {
2365                        // NOTE(aljoscha); Same as above. We want to be
2366                        // explicit.
2367                        //
2368                        // Also, technically for sinks there is no webhook
2369                        // "sources" that would force us to use an `Option`. But
2370                        // we still have to use an option for other reasons: the
2371                        // scraper expects a trait for working with stats, and
2372                        // that in the end forces the sink stats map to also
2373                        // have `Option` in the key. Do I like that? No, but
2374                        // here we are.
2375                        let replica_id = if let Some(replica_id) = replica_id {
2376                            replica_id
2377                        } else {
2378                            tracing::error!(
2379                                ?sink_stats,
2380                                "missing replica_id for sink statistics update"
2381                            );
2382                            continue;
2383                        };
2384
2385                        let mut shared_stats = self.sink_statistics.lock().expect("poisoned");
2386
2387                        for stat in sink_stats {
2388                            let collection_id = stat.id.clone();
2389
2390                            if self.collection(collection_id).is_err() {
2391                                // We can get updates for collections that have
2392                                // already been deleted, ignore those.
2393                                continue;
2394                            }
2395
2396                            let entry = shared_stats.entry((stat.id, Some(replica_id)));
2397
2398                            match entry {
2399                                btree_map::Entry::Vacant(vacant_entry) => {
2400                                    let mut stats =
2401                                        ControllerSinkStatistics::new(collection_id, replica_id);
2402                                    stats.incorporate(stat);
2403                                    vacant_entry.insert(stats);
2404                                }
2405                                btree_map::Entry::Occupied(mut occupied_entry) => {
2406                                    occupied_entry.get_mut().incorporate(stat);
2407                                }
2408                            }
2409                        }
2410                    }
2411                }
2412                (replica_id, StorageResponse::StatusUpdate(mut status_update)) => {
2413                    // NOTE(aljoscha): We sniff out the hydration status for
2414                    // ingestions from status updates. This is the easiest we
2415                    // can do right now, without going deeper into changing the
2416                    // comms protocol between controller and cluster. We cannot,
2417                    // for example use `StorageResponse::FrontierUpper`,
2418                    // because those will already get sent when the ingestion is
2419                    // just being created.
2420                    //
2421                    // Sources differ in when they will report as Running. Kafka
2422                    // UPSERT sources will only switch to `Running` once their
2423                    // state has been initialized from persist, which is the
2424                    // first case that we care about right now.
2425                    //
2426                    // I wouldn't say it's ideal, but it's workable until we
2427                    // find something better.
2428                    match status_update.status {
2429                        Status::Running => {
2430                            let collection = self.collections.get_mut(&status_update.id);
2431                            match collection {
2432                                Some(collection) => {
2433                                    match collection.extra_state {
2434                                        CollectionStateExtra::Ingestion(
2435                                            ref mut ingestion_state,
2436                                        ) => {
2437                                            if ingestion_state.hydrated_on.is_empty() {
2438                                                tracing::debug!(ingestion_id = %status_update.id, "ingestion is hydrated");
2439                                            }
2440                                            ingestion_state.hydrated_on.insert(replica_id.expect(
2441                                                "replica id should be present for status running",
2442                                            ));
2443                                        }
2444                                        CollectionStateExtra::Export(_) => {
2445                                            // TODO(sinks): track sink hydration?
2446                                        }
2447                                        CollectionStateExtra::None => {
2448                                            // Nothing to do
2449                                        }
2450                                    }
2451                                }
2452                                None => (), // no collection, let's say that's fine
2453                                            // here
2454                            }
2455                        }
2456                        Status::Paused => {
2457                            let collection = self.collections.get_mut(&status_update.id);
2458                            match collection {
2459                                Some(collection) => {
2460                                    match collection.extra_state {
2461                                        CollectionStateExtra::Ingestion(
2462                                            ref mut ingestion_state,
2463                                        ) => {
2464                                            // TODO: Paused gets send when there
2465                                            // are no active replicas. We should
2466                                            // change this to send a targeted
2467                                            // Pause for each replica, and do
2468                                            // more fine-grained hydration
2469                                            // tracking here.
2470                                            tracing::debug!(ingestion_id = %status_update.id, "ingestion is now paused");
2471                                            ingestion_state.hydrated_on.clear();
2472                                        }
2473                                        CollectionStateExtra::Export(_) => {
2474                                            // TODO(sinks): track sink hydration?
2475                                        }
2476                                        CollectionStateExtra::None => {
2477                                            // Nothing to do
2478                                        }
2479                                    }
2480                                }
2481                                None => (), // no collection, let's say that's fine
2482                                            // here
2483                            }
2484                        }
2485                        _ => (),
2486                    }
2487
2488                    // Set replica_id in the status update if available
2489                    if let Some(id) = replica_id {
2490                        status_update.replica_id = Some(id);
2491                    }
2492                    status_updates.push(status_update);
2493                }
2494                (_replica_id, StorageResponse::StagedBatches(batches)) => {
2495                    for (ingestion_id, batches) in batches {
2496                        match self.pending_oneshot_ingestions.remove(&ingestion_id) {
2497                            Some(pending) => {
2498                                // Send a cancel command so our command history is correct. And to
2499                                // avoid duplicate work once we have active replication.
2500                                if let Some(instance) = self.instances.get_mut(&pending.cluster_id)
2501                                {
2502                                    instance
2503                                        .send(StorageCommand::CancelOneshotIngestion(ingestion_id));
2504                                }
2505                                // Send the results down our channel.
2506                                (pending.result_tx)(batches)
2507                            }
2508                            None => {
2509                                // We might not be tracking this oneshot ingestion anymore because
2510                                // it was canceled.
2511                            }
2512                        }
2513                    }
2514                }
2515            }
2516        }
2517
2518        self.record_status_updates(status_updates);
2519
2520        // Process dropped tables in a single batch.
2521        let mut dropped_table_ids = Vec::new();
2522        while let Ok(dropped_id) = self.pending_table_handle_drops_rx.try_recv() {
2523            dropped_table_ids.push(dropped_id);
2524        }
2525        if !dropped_table_ids.is_empty() {
2526            self.drop_sources(storage_metadata, dropped_table_ids)?;
2527        }
2528
2529        if updated_frontiers.is_empty() {
2530            Ok(None)
2531        } else {
2532            Ok(Some(Response::FrontierUpdates(
2533                updated_frontiers.into_iter().collect(),
2534            )))
2535        }
2536    }
2537
2538    async fn inspect_persist_state(
2539        &self,
2540        id: GlobalId,
2541    ) -> Result<serde_json::Value, anyhow::Error> {
2542        let collection = &self.storage_collections.collection_metadata(id)?;
2543        let client = self
2544            .persist
2545            .open(collection.persist_location.clone())
2546            .await?;
2547        let shard_state = client
2548            .inspect_shard::<Timestamp>(&collection.data_shard)
2549            .await?;
2550        let json_state = serde_json::to_value(shard_state)?;
2551        Ok(json_state)
2552    }
2553
2554    fn append_introspection_updates(
2555        &mut self,
2556        type_: IntrospectionType,
2557        updates: Vec<(Row, Diff)>,
2558    ) {
2559        let id = self.introspection_ids[&type_];
2560        let updates = updates.into_iter().map(|update| update.into()).collect();
2561        self.collection_manager.blind_write(id, updates);
2562    }
2563
2564    fn append_status_introspection_updates(
2565        &mut self,
2566        type_: IntrospectionType,
2567        updates: Vec<StatusUpdate>,
2568    ) {
2569        let id = self.introspection_ids[&type_];
2570        let updates: Vec<_> = updates.into_iter().map(|update| update.into()).collect();
2571        if !updates.is_empty() {
2572            self.collection_manager.blind_write(id, updates);
2573        }
2574    }
2575
2576    fn update_introspection_collection(&mut self, type_: IntrospectionType, op: StorageWriteOp) {
2577        let id = self.introspection_ids[&type_];
2578        self.collection_manager.differential_write(id, op);
2579    }
2580
2581    fn append_only_introspection_tx(
2582        &self,
2583        type_: IntrospectionType,
2584    ) -> mpsc::UnboundedSender<(
2585        Vec<AppendOnlyUpdate>,
2586        oneshot::Sender<Result<(), StorageError>>,
2587    )> {
2588        let id = self.introspection_ids[&type_];
2589        self.collection_manager.append_only_write_sender(id)
2590    }
2591
2592    fn differential_introspection_tx(
2593        &self,
2594        type_: IntrospectionType,
2595    ) -> mpsc::UnboundedSender<(StorageWriteOp, oneshot::Sender<Result<(), StorageError>>)> {
2596        let id = self.introspection_ids[&type_];
2597        self.collection_manager.differential_write_sender(id)
2598    }
2599
2600    async fn real_time_recent_timestamp(
2601        &self,
2602        timestamp_objects: BTreeSet<GlobalId>,
2603        timeout: Duration,
2604    ) -> Result<BoxFuture<Result<Timestamp, StorageError>>, StorageError> {
2605        use mz_storage_types::sources::GenericSourceConnection;
2606
2607        let mut rtr_futures = BTreeMap::new();
2608
2609        // Only user sources can be read from w/ RTR.
2610        for id in timestamp_objects.into_iter().filter(GlobalId::is_user) {
2611            let collection = match self.collection(id) {
2612                Ok(c) => c,
2613                // Not a storage item, which we accept.
2614                Err(_) => continue,
2615            };
2616
2617            let (source_conn, remap_id) = match &collection.data_source {
2618                DataSource::Ingestion(IngestionDescription {
2619                    desc: SourceDesc { connection, .. },
2620                    remap_collection_id,
2621                    ..
2622                }) => match connection {
2623                    GenericSourceConnection::Kafka(_)
2624                    | GenericSourceConnection::Postgres(_)
2625                    | GenericSourceConnection::MySql(_)
2626                    | GenericSourceConnection::SqlServer(_) => {
2627                        (connection.clone(), *remap_collection_id)
2628                    }
2629
2630                    // These internal sources do not yet (and might never)
2631                    // support RTR. However, erroring if they're selected from
2632                    // poses an annoying user experience, so instead just skip
2633                    // over them.
2634                    GenericSourceConnection::LoadGenerator(_) => continue,
2635                },
2636                // Skip over all other objects
2637                _ => {
2638                    continue;
2639                }
2640            };
2641
2642            // Prepare for getting the external system's frontier.
2643            let config = self.config().clone();
2644
2645            // Determine the remap collection we plan to read from.
2646            //
2647            // Note that the process of reading from the remap shard is the same
2648            // as other areas in this code that do the same thing, but we inline
2649            // it here because we must prove that we have not taken ownership of
2650            // `self` to move the stream of data from the remap shard into a
2651            // future.
2652            let read_handle = self.read_handle_for_snapshot(remap_id).await?;
2653
2654            // Have to acquire a read hold to prevent the since from advancing
2655            // while we read.
2656            let remap_read_hold = self
2657                .storage_collections
2658                .acquire_read_holds(vec![remap_id])
2659                .map_err(|_e| StorageError::ReadBeforeSince(remap_id))?
2660                .expect_element(|| "known to be exactly one");
2661
2662            let remap_as_of = remap_read_hold
2663                .since()
2664                .to_owned()
2665                .into_option()
2666                .ok_or(StorageError::ReadBeforeSince(remap_id))?;
2667
2668            rtr_futures.insert(
2669                id,
2670                tokio::time::timeout(timeout, async move {
2671                    use mz_storage_types::sources::SourceConnection as _;
2672
2673                    // Fetch the remap shard's contents; we must do this first so
2674                    // that the `as_of` doesn't change.
2675                    let as_of = Antichain::from_elem(remap_as_of);
2676                    let remap_subscribe = read_handle
2677                        .subscribe(as_of.clone())
2678                        .await
2679                        .map_err(|_| StorageError::ReadBeforeSince(remap_id))?;
2680
2681                    tracing::debug!(?id, type_ = source_conn.name(), upstream = ?source_conn.external_reference(), "fetching real time recency");
2682
2683                    let result = rtr::real_time_recency_ts(
2684                        source_conn,
2685                        id,
2686                        config,
2687                        as_of,
2688                        remap_subscribe,
2689                    )
2690                    .await
2691                    .map_err(|e| {
2692                            tracing::debug!(?id, "real time recency error: {:?}", e);
2693                            e
2694                        });
2695
2696                    // Drop once we have read succesfully.
2697                    drop(remap_read_hold);
2698
2699                    result
2700                }),
2701            );
2702        }
2703
2704        Ok(Box::pin(async move {
2705            let (ids, futs): (Vec<_>, Vec<_>) = rtr_futures.into_iter().unzip();
2706            ids.into_iter()
2707                .zip_eq(futures::future::join_all(futs).await)
2708                .try_fold(Timestamp::MIN, |curr, (id, per_source_res)| {
2709                    let new =
2710                        per_source_res.map_err(|_e: Elapsed| StorageError::RtrTimeout(id))??;
2711                    Ok::<_, StorageError>(std::cmp::max(curr, new))
2712                })
2713        }))
2714    }
2715
2716    fn dump(&self) -> Result<serde_json::Value, anyhow::Error> {
2717        // Destructure `self` here so we don't forget to consider dumping newly added fields.
2718        let Self {
2719            build_info: _,
2720            now: _,
2721            read_only,
2722            collections,
2723            dropped_objects,
2724            persist_table_worker: _,
2725            txns_read: _,
2726            txns_metrics: _,
2727            stashed_responses,
2728            pending_table_handle_drops_tx: _,
2729            pending_table_handle_drops_rx: _,
2730            pending_oneshot_ingestions,
2731            collection_manager: _,
2732            introspection_ids,
2733            introspection_tokens: _,
2734            source_statistics: _,
2735            sink_statistics: _,
2736            statistics_interval_sender: _,
2737            instances,
2738            replica_dyncfg_overrides: _,
2739            initialized,
2740            config,
2741            persist_location,
2742            persist: _,
2743            metrics: _,
2744            recorded_frontiers,
2745            recorded_replica_frontiers,
2746            wallclock_lag: _,
2747            wallclock_lag_last_recorded,
2748            storage_collections: _,
2749            migrated_storage_collections,
2750            maintenance_ticker: _,
2751            maintenance_scheduled,
2752            instance_response_tx: _,
2753            instance_response_rx: _,
2754            persist_warm_task: _,
2755        } = self;
2756
2757        let collections: BTreeMap<_, _> = collections
2758            .iter()
2759            .map(|(id, c)| (id.to_string(), format!("{c:?}")))
2760            .collect();
2761        let dropped_objects: BTreeMap<_, _> = dropped_objects
2762            .iter()
2763            .map(|(id, rs)| (id.to_string(), format!("{rs:?}")))
2764            .collect();
2765        let stashed_responses: Vec<_> =
2766            stashed_responses.iter().map(|r| format!("{r:?}")).collect();
2767        let pending_oneshot_ingestions: BTreeMap<_, _> = pending_oneshot_ingestions
2768            .iter()
2769            .map(|(uuid, i)| (uuid.to_string(), format!("{i:?}")))
2770            .collect();
2771        let introspection_ids: BTreeMap<_, _> = introspection_ids
2772            .iter()
2773            .map(|(typ, id)| (format!("{typ:?}"), id.to_string()))
2774            .collect();
2775        let instances: BTreeMap<_, _> = instances
2776            .iter()
2777            .map(|(id, i)| (id.to_string(), format!("{i:?}")))
2778            .collect();
2779        let recorded_frontiers: BTreeMap<_, _> = recorded_frontiers
2780            .iter()
2781            .map(|(id, fs)| (id.to_string(), format!("{fs:?}")))
2782            .collect();
2783        let recorded_replica_frontiers: Vec<_> = recorded_replica_frontiers
2784            .iter()
2785            .map(|((gid, rid), f)| (gid.to_string(), rid.to_string(), format!("{f:?}")))
2786            .collect();
2787        let migrated_storage_collections: Vec<_> = migrated_storage_collections
2788            .iter()
2789            .map(|id| id.to_string())
2790            .collect();
2791
2792        Ok(serde_json::json!({
2793            "read_only": read_only,
2794            "collections": collections,
2795            "dropped_objects": dropped_objects,
2796            "stashed_responses": stashed_responses,
2797            "pending_oneshot_ingestions": pending_oneshot_ingestions,
2798            "introspection_ids": introspection_ids,
2799            "instances": instances,
2800            "initialized": initialized,
2801            "config": format!("{config:?}"),
2802            "persist_location": format!("{persist_location:?}"),
2803            "recorded_frontiers": recorded_frontiers,
2804            "recorded_replica_frontiers": recorded_replica_frontiers,
2805            "wallclock_lag_last_recorded": format!("{wallclock_lag_last_recorded:?}"),
2806            "migrated_storage_collections": migrated_storage_collections,
2807            "maintenance_scheduled": maintenance_scheduled,
2808        }))
2809    }
2810}
2811
2812/// Seed [`StorageTxn`] with any state required to instantiate a
2813/// [`StorageController`].
2814///
2815/// This cannot be a member of [`StorageController`] because it cannot take a
2816/// `self` parameter.
2817///
2818pub fn prepare_initialization(txn: &mut dyn StorageTxn) -> Result<(), StorageError> {
2819    if txn.get_txn_wal_shard().is_none() {
2820        let txns_id = ShardId::new();
2821        txn.write_txn_wal_shard(txns_id)?;
2822    }
2823
2824    Ok(())
2825}
2826
2827impl Controller
2828where
2829    Self: StorageController,
2830{
2831    /// Create a new storage controller from a client it should wrap.
2832    ///
2833    /// Note that when creating a new storage controller, you must also
2834    /// reconcile it with the previous state.
2835    ///
2836    /// # Panics
2837    /// If this function is called before [`prepare_initialization`].
2838    pub async fn new(
2839        build_info: &'static BuildInfo,
2840        persist_location: PersistLocation,
2841        persist_clients: Arc<PersistClientCache>,
2842        now: NowFn,
2843        wallclock_lag: WallclockLagFn<Timestamp>,
2844        txns_metrics: Arc<TxnMetrics>,
2845        read_only: bool,
2846        metrics_registry: &MetricsRegistry,
2847        controller_metrics: ControllerMetrics,
2848        connection_context: ConnectionContext,
2849        txn: &dyn StorageTxn,
2850        storage_collections: Arc<dyn StorageCollections + Send + Sync>,
2851    ) -> Self {
2852        let txns_client = persist_clients
2853            .open(persist_location.clone())
2854            .await
2855            .expect("location should be valid");
2856
2857        let persist_warm_task = warm_persist_state_in_background(
2858            txns_client.clone(),
2859            txn.get_collection_metadata().into_values(),
2860        );
2861        let persist_warm_task = Some(persist_warm_task.abort_on_drop());
2862
2863        // This value must be already installed because we must ensure it's
2864        // durably recorded before it is used, otherwise we risk leaking persist
2865        // state.
2866        let txns_id = txn
2867            .get_txn_wal_shard()
2868            .expect("must call prepare initialization before creating storage controller");
2869
2870        let persist_table_worker = if read_only {
2871            let txns_write = txns_client
2872                .open_writer(
2873                    txns_id,
2874                    Arc::new(TxnsCodecRow::desc()),
2875                    Arc::new(UnitSchema),
2876                    Diagnostics {
2877                        shard_name: "txns".to_owned(),
2878                        handle_purpose: "follow txns upper".to_owned(),
2879                    },
2880                )
2881                .await
2882                .expect("txns schema shouldn't change");
2883            persist_handles::PersistTableWriteWorker::new_read_only_mode(
2884                txns_write,
2885                txns_client.clone(),
2886            )
2887        } else {
2888            let mut txns = TxnsHandle::open(
2889                Timestamp::MIN,
2890                txns_client.clone(),
2891                txns_client.dyncfgs().clone(),
2892                Arc::clone(&txns_metrics),
2893                txns_id,
2894                Opaque::encode(&PersistEpoch::default()),
2895            )
2896            .await;
2897            txns.upgrade_version().await;
2898            persist_handles::PersistTableWriteWorker::new_txns(txns, txns_client.clone())
2899        };
2900        let txns_read = TxnsRead::start::<TxnsCodecRow>(txns_client.clone(), txns_id).await;
2901
2902        let collection_manager = collection_mgmt::CollectionManager::new(read_only, now.clone());
2903
2904        let introspection_ids = BTreeMap::new();
2905        let introspection_tokens = Arc::new(Mutex::new(BTreeMap::new()));
2906
2907        let (statistics_interval_sender, _) =
2908            channel(mz_storage_types::parameters::STATISTICS_INTERVAL_DEFAULT);
2909
2910        let (pending_table_handle_drops_tx, pending_table_handle_drops_rx) =
2911            tokio::sync::mpsc::unbounded_channel();
2912
2913        let mut maintenance_ticker = tokio::time::interval(Duration::from_secs(1));
2914        maintenance_ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
2915
2916        let (instance_response_tx, instance_response_rx) = mpsc::unbounded_channel();
2917
2918        let metrics = StorageControllerMetrics::new(metrics_registry, controller_metrics);
2919
2920        let now_dt = mz_ore::now::to_datetime(now());
2921
2922        Self {
2923            build_info,
2924            collections: BTreeMap::default(),
2925            dropped_objects: Default::default(),
2926            persist_table_worker,
2927            txns_read,
2928            txns_metrics,
2929            stashed_responses: vec![],
2930            pending_table_handle_drops_tx,
2931            pending_table_handle_drops_rx,
2932            pending_oneshot_ingestions: BTreeMap::default(),
2933            collection_manager,
2934            introspection_ids,
2935            introspection_tokens,
2936            now,
2937            read_only,
2938            source_statistics: Arc::new(Mutex::new(statistics::SourceStatistics {
2939                source_statistics: BTreeMap::new(),
2940                webhook_statistics: BTreeMap::new(),
2941            })),
2942            sink_statistics: Arc::new(Mutex::new(BTreeMap::new())),
2943            statistics_interval_sender,
2944            instances: BTreeMap::new(),
2945            replica_dyncfg_overrides: BTreeMap::new(),
2946            initialized: false,
2947            config: StorageConfiguration::new(connection_context, mz_dyncfgs::all_dyncfgs()),
2948            persist_location,
2949            persist: persist_clients,
2950            metrics,
2951            recorded_frontiers: BTreeMap::new(),
2952            recorded_replica_frontiers: BTreeMap::new(),
2953            wallclock_lag,
2954            wallclock_lag_last_recorded: now_dt,
2955            storage_collections,
2956            migrated_storage_collections: BTreeSet::new(),
2957            maintenance_ticker,
2958            maintenance_scheduled: false,
2959            instance_response_rx,
2960            instance_response_tx,
2961            persist_warm_task,
2962        }
2963    }
2964
2965    // This is different from `set_read_policies`, which is for external users.
2966    // This method is for setting the policy that the controller uses when
2967    // maintaining the read holds that it has for collections/exports at the
2968    // StorageCollections.
2969    //
2970    // This is really only used when dropping things, where we set the
2971    // ReadPolicy to the empty Antichain.
2972    #[instrument(level = "debug")]
2973    fn set_hold_policies(&mut self, policies: Vec<(GlobalId, ReadPolicy)>) {
2974        let mut read_capability_changes = BTreeMap::default();
2975
2976        for (id, policy) in policies.into_iter() {
2977            if let Some(collection) = self.collections.get_mut(&id) {
2978                let (write_frontier, derived_since, hold_policy) = match &mut collection.extra_state
2979                {
2980                    CollectionStateExtra::Ingestion(ingestion) => (
2981                        ingestion.write_frontier.borrow(),
2982                        &mut ingestion.derived_since,
2983                        &mut ingestion.hold_policy,
2984                    ),
2985                    CollectionStateExtra::None => {
2986                        unreachable!("set_hold_policies is only called for ingestions");
2987                    }
2988                    CollectionStateExtra::Export(export) => (
2989                        export.write_frontier.borrow(),
2990                        &mut export.derived_since,
2991                        &mut export.read_policy,
2992                    ),
2993                };
2994
2995                let new_derived_since = policy.frontier(write_frontier);
2996                let mut update = swap_updates(derived_since, new_derived_since);
2997                if !update.is_empty() {
2998                    read_capability_changes.insert(id, update);
2999                }
3000
3001                *hold_policy = policy;
3002            }
3003        }
3004
3005        if !read_capability_changes.is_empty() {
3006            self.update_hold_capabilities(&mut read_capability_changes);
3007        }
3008    }
3009
3010    #[instrument(level = "debug", fields(updates))]
3011    fn update_write_frontier(&mut self, id: GlobalId, new_upper: &Antichain<Timestamp>) {
3012        let mut read_capability_changes = BTreeMap::default();
3013
3014        if let Some(collection) = self.collections.get_mut(&id) {
3015            let (write_frontier, derived_since, hold_policy) = match &mut collection.extra_state {
3016                CollectionStateExtra::Ingestion(ingestion) => (
3017                    &mut ingestion.write_frontier,
3018                    &mut ingestion.derived_since,
3019                    &ingestion.hold_policy,
3020                ),
3021                CollectionStateExtra::None => {
3022                    if matches!(collection.data_source, DataSource::Progress) {
3023                        // We do get these, but can't do anything with it!
3024                    } else {
3025                        tracing::error!(
3026                            ?collection,
3027                            ?new_upper,
3028                            "updated write frontier for collection which is not an ingestion"
3029                        );
3030                    }
3031                    return;
3032                }
3033                CollectionStateExtra::Export(export) => (
3034                    &mut export.write_frontier,
3035                    &mut export.derived_since,
3036                    &export.read_policy,
3037                ),
3038            };
3039
3040            if PartialOrder::less_than(write_frontier, new_upper) {
3041                write_frontier.clone_from(new_upper);
3042            }
3043
3044            let new_derived_since = hold_policy.frontier(write_frontier.borrow());
3045            let mut update = swap_updates(derived_since, new_derived_since);
3046            if !update.is_empty() {
3047                read_capability_changes.insert(id, update);
3048            }
3049        } else if self.dropped_objects.contains_key(&id) {
3050            // We dropped an object but might still get updates from cluster
3051            // side, before it notices the drop. This is expected and fine.
3052        } else {
3053            soft_panic_or_log!("spurious upper update for {id}: {new_upper:?}");
3054        }
3055
3056        if !read_capability_changes.is_empty() {
3057            self.update_hold_capabilities(&mut read_capability_changes);
3058        }
3059    }
3060
3061    // This is different from `update_read_capabilities`, which is for external users.
3062    // This method is for maintaining the read holds that the controller has at
3063    // the StorageCollections, for storage dependencies.
3064    #[instrument(level = "debug", fields(updates))]
3065    fn update_hold_capabilities(
3066        &mut self,
3067        updates: &mut BTreeMap<GlobalId, ChangeBatch<Timestamp>>,
3068    ) {
3069        // Location to record consequences that we need to act on.
3070        let mut collections_net = BTreeMap::new();
3071
3072        // We must not rely on any specific relative ordering of `GlobalId`s.
3073        // That said, it is reasonable to assume that collections generally have
3074        // greater IDs than their dependencies, so starting with the largest is
3075        // a useful optimization.
3076        while let Some(key) = updates.keys().rev().next().cloned() {
3077            let mut update = updates.remove(&key).unwrap();
3078
3079            if key.is_user() {
3080                debug!(id = %key, ?update, "update_hold_capability");
3081            }
3082
3083            if let Some(collection) = self.collections.get_mut(&key) {
3084                match &mut collection.extra_state {
3085                    CollectionStateExtra::Ingestion(ingestion) => {
3086                        let changes = ingestion.read_capabilities.update_iter(update.drain());
3087                        update.extend(changes);
3088
3089                        let (changes, frontier, _cluster_id) =
3090                            collections_net.entry(key).or_insert_with(|| {
3091                                (
3092                                    <ChangeBatch<_>>::new(),
3093                                    Antichain::new(),
3094                                    ingestion.instance_id,
3095                                )
3096                            });
3097
3098                        changes.extend(update.drain());
3099                        *frontier = ingestion.read_capabilities.frontier().to_owned();
3100                    }
3101                    CollectionStateExtra::None => {
3102                        // WIP: See if this ever panics in ci.
3103                        soft_panic_or_log!(
3104                            "trying to update holds for collection {collection:?} which is not \
3105                             an ingestion: {update:?}"
3106                        );
3107                        continue;
3108                    }
3109                    CollectionStateExtra::Export(export) => {
3110                        let changes = export.read_capabilities.update_iter(update.drain());
3111                        update.extend(changes);
3112
3113                        let (changes, frontier, _cluster_id) =
3114                            collections_net.entry(key).or_insert_with(|| {
3115                                (<ChangeBatch<_>>::new(), Antichain::new(), export.cluster_id)
3116                            });
3117
3118                        changes.extend(update.drain());
3119                        *frontier = export.read_capabilities.frontier().to_owned();
3120                    }
3121                }
3122            } else {
3123                // This is confusing and we should probably error.
3124                tracing::warn!(id = ?key, ?update, "update_hold_capabilities for unknown object");
3125            }
3126        }
3127
3128        // Translate our net compute actions into `AllowCompaction` commands and
3129        // downgrade persist sinces.
3130        for (key, (mut changes, frontier, cluster_id)) in collections_net {
3131            if !changes.is_empty() {
3132                if key.is_user() {
3133                    debug!(id = %key, ?frontier, "downgrading ingestion read holds!");
3134                }
3135
3136                let collection = self
3137                    .collections
3138                    .get_mut(&key)
3139                    .expect("missing collection state");
3140
3141                let read_holds = match &mut collection.extra_state {
3142                    CollectionStateExtra::Ingestion(ingestion) => {
3143                        ingestion.dependency_read_holds.as_mut_slice()
3144                    }
3145                    CollectionStateExtra::Export(export) => export.read_holds.as_mut_slice(),
3146                    CollectionStateExtra::None => {
3147                        soft_panic_or_log!(
3148                            "trying to downgrade read holds for collection which is not an \
3149                             ingestion: {collection:?}"
3150                        );
3151                        continue;
3152                    }
3153                };
3154
3155                for read_hold in read_holds.iter_mut() {
3156                    read_hold
3157                        .try_downgrade(frontier.clone())
3158                        .expect("we only advance the frontier");
3159                }
3160
3161                // Send AllowCompaction command directly to the instance
3162                if let Some(instance) = self.instances.get_mut(&cluster_id) {
3163                    instance.send(StorageCommand::AllowCompaction(key, frontier.clone()));
3164                } else {
3165                    soft_panic_or_log!(
3166                        "missing instance client for cluster {cluster_id} while we still have outstanding AllowCompaction command {frontier:?} for {key}"
3167                    );
3168                }
3169            }
3170        }
3171    }
3172
3173    /// Validate that a collection exists for all identifiers, and error if any do not.
3174    fn validate_collection_ids(
3175        &self,
3176        ids: impl Iterator<Item = GlobalId>,
3177    ) -> Result<(), StorageError> {
3178        for id in ids {
3179            self.storage_collections.check_exists(id)?;
3180        }
3181        Ok(())
3182    }
3183
3184    /// Validate that a collection exists for all identifiers, and error if any do not.
3185    fn validate_export_ids(&self, ids: impl Iterator<Item = GlobalId>) -> Result<(), StorageError> {
3186        for id in ids {
3187            self.export(id)?;
3188        }
3189        Ok(())
3190    }
3191
3192    /// Opens a write handle and synchronizes its cached upper.
3193    async fn open_data_handles(
3194        &self,
3195        id: &GlobalId,
3196        shard: ShardId,
3197        relation_desc: RelationDesc,
3198        persist_client: &PersistClient,
3199    ) -> WriteHandle<SourceData, (), Timestamp, StorageDiff> {
3200        let diagnostics = Diagnostics {
3201            shard_name: id.to_string(),
3202            handle_purpose: format!("controller data for {}", id),
3203        };
3204
3205        let mut write = persist_client
3206            .open_writer(
3207                shard,
3208                Arc::new(relation_desc),
3209                Arc::new(UnitSchema),
3210                diagnostics.clone(),
3211            )
3212            .await
3213            .expect("invalid persist usage");
3214
3215        // Synchronize the cached upper before it initializes collection state.
3216        write.fetch_recent_upper().await;
3217
3218        write
3219    }
3220
3221    /// Registers the given introspection collection and does any preparatory
3222    /// work that we have to do before we start writing to it. This
3223    /// preparatory work will include partial truncation or other cleanup
3224    /// schemes, depending on introspection type.
3225    fn register_introspection_collection(
3226        &mut self,
3227        id: GlobalId,
3228        introspection_type: IntrospectionType,
3229        write_handle: WriteHandle<SourceData, (), Timestamp, StorageDiff>,
3230        persist_client: PersistClient,
3231    ) -> Result<(), StorageError> {
3232        tracing::info!(%id, ?introspection_type, "registering introspection collection");
3233
3234        // In read-only mode we create a new shard for all migrated storage collections. So we
3235        // "trick" the write task into thinking that it's not in read-only mode so something is
3236        // advancing this new shard.
3237        //
3238        // This is the storage-side of forcing writes to a self-owned replacement shard while
3239        // read-only. Migrated builtin MVs need the same treatment on their own write path; see
3240        // `ComputeController::allow_writes_in_read_only`.
3241        let force_writable = self.read_only && self.migrated_storage_collections.contains(&id);
3242        if force_writable {
3243            assert!(id.is_system(), "unexpected non-system global id: {id:?}");
3244            info!("writing to migrated storage collection {id} in read-only mode");
3245        }
3246
3247        let prev = self.introspection_ids.insert(introspection_type, id);
3248        assert!(
3249            prev.is_none(),
3250            "cannot have multiple IDs for introspection type"
3251        );
3252
3253        let metadata = self.storage_collections.collection_metadata(id)?.clone();
3254
3255        let read_handle_fn = move || {
3256            let persist_client = persist_client.clone();
3257            let metadata = metadata.clone();
3258
3259            let fut = async move {
3260                let read_handle = persist_client
3261                    .open_leased_reader::<SourceData, (), Timestamp, StorageDiff>(
3262                        metadata.data_shard,
3263                        Arc::new(metadata.relation_desc.clone()),
3264                        Arc::new(UnitSchema),
3265                        Diagnostics {
3266                            shard_name: id.to_string(),
3267                            handle_purpose: format!("snapshot {}", id),
3268                        },
3269                        USE_CRITICAL_SINCE_SNAPSHOT.get(persist_client.dyncfgs()),
3270                    )
3271                    .await
3272                    .expect("invalid persist usage");
3273                read_handle
3274            };
3275
3276            fut.boxed()
3277        };
3278
3279        let recent_upper = write_handle.shared_upper();
3280
3281        match CollectionManagerKind::from(&introspection_type) {
3282            // For these, we first register the collection and then prepare it,
3283            // because the code that prepares differential collection expects to
3284            // be able to update desired state via the collection manager
3285            // already.
3286            CollectionManagerKind::Differential => {
3287                let statistics_retention_duration =
3288                    dyncfgs::STATISTICS_RETENTION_DURATION.get(self.config().config_set());
3289
3290                // These do a shallow copy.
3291                let introspection_config = DifferentialIntrospectionConfig {
3292                    recent_upper,
3293                    introspection_type,
3294                    storage_collections: Arc::clone(&self.storage_collections),
3295                    collection_manager: self.collection_manager.clone(),
3296                    source_statistics: Arc::clone(&self.source_statistics),
3297                    sink_statistics: Arc::clone(&self.sink_statistics),
3298                    statistics_interval: self.config.parameters.statistics_interval.clone(),
3299                    statistics_interval_receiver: self.statistics_interval_sender.subscribe(),
3300                    statistics_retention_duration,
3301                    metrics: self.metrics.clone(),
3302                    introspection_tokens: Arc::clone(&self.introspection_tokens),
3303                };
3304                self.collection_manager.register_differential_collection(
3305                    id,
3306                    write_handle,
3307                    read_handle_fn,
3308                    force_writable,
3309                    introspection_config,
3310                );
3311            }
3312            // For these, we first have to prepare and then register with
3313            // collection manager, because the preparation logic wants to read
3314            // the shard's contents and then do uncontested writes.
3315            //
3316            // TODO(aljoscha): We should make the truncation/cleanup work that
3317            // happens when we take over instead be a periodic thing, and make
3318            // it resilient to the upper moving concurrently.
3319            CollectionManagerKind::AppendOnly => {
3320                let introspection_config = AppendOnlyIntrospectionConfig {
3321                    introspection_type,
3322                    config_set: Arc::clone(self.config.config_set()),
3323                    parameters: self.config.parameters.clone(),
3324                    storage_collections: Arc::clone(&self.storage_collections),
3325                };
3326                self.collection_manager.register_append_only_collection(
3327                    id,
3328                    write_handle,
3329                    force_writable,
3330                    Some(introspection_config),
3331                );
3332            }
3333        }
3334
3335        Ok(())
3336    }
3337
3338    /// Remove statistics for sources/sinks that were dropped but still have statistics rows
3339    /// hanging around.
3340    fn reconcile_dangling_statistics(&self) {
3341        self.source_statistics
3342            .lock()
3343            .expect("poisoned")
3344            .source_statistics
3345            // collections should also contain subsources.
3346            .retain(|(k, _replica_id), _| self.storage_collections.check_exists(*k).is_ok());
3347        self.sink_statistics
3348            .lock()
3349            .expect("poisoned")
3350            .retain(|(k, _replica_id), _| self.export(*k).is_ok());
3351    }
3352
3353    /// Appends a new global ID, shard ID pair to the appropriate collection.
3354    /// Use a `diff` of 1 to append a new entry; -1 to retract an existing
3355    /// entry.
3356    ///
3357    /// # Panics
3358    /// - If `self.collections` does not have an entry for `global_id`.
3359    /// - If `IntrospectionType::ShardMapping`'s `GlobalId` is not registered as
3360    ///   a managed collection.
3361    /// - If diff is any value other than `1` or `-1`.
3362    #[instrument(level = "debug")]
3363    fn append_shard_mappings<I>(&self, global_ids: I, diff: Diff)
3364    where
3365        I: Iterator<Item = GlobalId>,
3366    {
3367        mz_ore::soft_assert_or_log!(
3368            diff == Diff::MINUS_ONE || diff == Diff::ONE,
3369            "use 1 for insert or -1 for delete"
3370        );
3371
3372        let id = *self
3373            .introspection_ids
3374            .get(&IntrospectionType::ShardMapping)
3375            .expect("should be registered before this call");
3376
3377        let mut updates = vec![];
3378        // Pack updates into rows
3379        let mut row_buf = Row::default();
3380
3381        for global_id in global_ids {
3382            let shard_id = if let Some(collection) = self.collections.get(&global_id) {
3383                collection.collection_metadata.data_shard.clone()
3384            } else {
3385                panic!("unknown global id: {}", global_id);
3386            };
3387
3388            let mut packer = row_buf.packer();
3389            packer.push(Datum::from(global_id.to_string().as_str()));
3390            packer.push(Datum::from(shard_id.to_string().as_str()));
3391            updates.push((row_buf.clone(), diff));
3392        }
3393
3394        self.collection_manager.differential_append(id, updates);
3395    }
3396
3397    /// Determines and returns this collection's dependencies, if any.
3398    fn determine_collection_dependencies(
3399        &self,
3400        self_id: GlobalId,
3401        collection_desc: &CollectionDescription,
3402    ) -> Result<Vec<GlobalId>, StorageError> {
3403        let mut dependencies = Vec::new();
3404
3405        if let Some(id) = collection_desc.primary {
3406            dependencies.push(id);
3407        }
3408
3409        match &collection_desc.data_source {
3410            DataSource::Introspection(_)
3411            | DataSource::Webhook
3412            | DataSource::Table
3413            | DataSource::Progress
3414            | DataSource::Other => (),
3415            DataSource::IngestionExport { ingestion_id, .. } => {
3416                // Ingestion exports depend on their primary source's remap
3417                // collection.
3418                let source_collection = self.collection(*ingestion_id)?;
3419                let ingestion_remap_collection_id = match &source_collection.data_source {
3420                    DataSource::Ingestion(ingestion) => ingestion.remap_collection_id,
3421                    _ => unreachable!(
3422                        "SourceExport must only refer to primary sources that already exist"
3423                    ),
3424                };
3425
3426                // Ingestion exports (aka. subsources) must make sure that 1)
3427                // their own collection's since stays one step behind the upper,
3428                // and, 2) that the remap shard's since stays one step behind
3429                // their upper. Hence they track themselves and the remap shard
3430                // as dependencies.
3431                dependencies.extend([self_id, ingestion_remap_collection_id]);
3432            }
3433            // Ingestions depend on their remap collection.
3434            DataSource::Ingestion(ingestion) => {
3435                // Ingestions must make sure that 1) their own collection's
3436                // since stays one step behind the upper, and, 2) that the remap
3437                // shard's since stays one step behind their upper. Hence they
3438                // track themselves and the remap shard as dependencies.
3439                dependencies.push(self_id);
3440                if self_id != ingestion.remap_collection_id {
3441                    dependencies.push(ingestion.remap_collection_id);
3442                }
3443            }
3444            DataSource::Sink { desc } => {
3445                // Sinks hold back their own frontier and the frontier of their input.
3446                dependencies.extend([self_id, desc.sink.from]);
3447            }
3448        };
3449
3450        Ok(dependencies)
3451    }
3452
3453    async fn read_handle_for_snapshot(
3454        &self,
3455        id: GlobalId,
3456    ) -> Result<ReadHandle<SourceData, (), Timestamp, StorageDiff>, StorageError> {
3457        let metadata = self.storage_collections.collection_metadata(id)?;
3458        read_handle_for_snapshot(&self.persist, id, &metadata).await
3459    }
3460
3461    /// Handles writing of status updates for sources/sinks to the appropriate
3462    /// status relation
3463    fn record_status_updates(&mut self, updates: Vec<StatusUpdate>) {
3464        if self.read_only {
3465            return;
3466        }
3467
3468        let mut sink_status_updates = vec![];
3469        let mut source_status_updates = vec![];
3470
3471        for update in updates {
3472            let id = update.id;
3473            if self.export(id).is_ok() {
3474                sink_status_updates.push(update);
3475            } else if self.storage_collections.check_exists(id).is_ok() {
3476                source_status_updates.push(update);
3477            }
3478        }
3479
3480        self.append_status_introspection_updates(
3481            IntrospectionType::SourceStatusHistory,
3482            source_status_updates,
3483        );
3484        self.append_status_introspection_updates(
3485            IntrospectionType::SinkStatusHistory,
3486            sink_status_updates,
3487        );
3488    }
3489
3490    fn collection(&self, id: GlobalId) -> Result<&CollectionState, StorageError> {
3491        self.collections
3492            .get(&id)
3493            .ok_or(StorageError::IdentifierMissing(id))
3494    }
3495
3496    /// Runs the identified ingestion using the current definition of the
3497    /// ingestion in-memory.
3498    fn run_ingestion(&mut self, id: GlobalId) -> Result<(), StorageError> {
3499        tracing::info!(%id, "starting ingestion");
3500
3501        let collection = self.collection(id)?;
3502        let ingestion_description = match &collection.data_source {
3503            DataSource::Ingestion(i) => i.clone(),
3504            _ => {
3505                tracing::warn!("run_ingestion called on non-ingestion ID {}", id);
3506                Err(StorageError::IdentifierInvalid(id))?
3507            }
3508        };
3509
3510        // Enrich all of the exports with their metadata
3511        let mut source_exports = BTreeMap::new();
3512        for (export_id, export) in ingestion_description.source_exports.clone() {
3513            let export_storage_metadata = self.collection(export_id)?.collection_metadata.clone();
3514            source_exports.insert(
3515                export_id,
3516                SourceExport {
3517                    storage_metadata: export_storage_metadata,
3518                    details: export.details,
3519                    data_config: export.data_config,
3520                },
3521            );
3522        }
3523
3524        let remap_collection = self.collection(ingestion_description.remap_collection_id)?;
3525
3526        let description = IngestionDescription::<CollectionMetadata> {
3527            source_exports,
3528            remap_metadata: remap_collection.collection_metadata.clone(),
3529            // The rest of the fields are identical
3530            desc: ingestion_description.desc.clone(),
3531            instance_id: ingestion_description.instance_id,
3532            remap_collection_id: ingestion_description.remap_collection_id,
3533        };
3534
3535        let storage_instance_id = description.instance_id;
3536        // Fetch the client for this ingestion's instance.
3537        let instance = self
3538            .instances
3539            .get_mut(&storage_instance_id)
3540            .ok_or_else(|| StorageError::IngestionInstanceMissing {
3541                storage_instance_id,
3542                ingestion_id: id,
3543            })?;
3544
3545        let augmented_ingestion = Box::new(RunIngestionCommand { id, description });
3546        instance.send(StorageCommand::RunIngestion(augmented_ingestion));
3547
3548        Ok(())
3549    }
3550
3551    /// Runs the identified export using the current definition of the export
3552    /// that we have in memory.
3553    fn run_export(&mut self, id: GlobalId) -> Result<(), StorageError> {
3554        let DataSource::Sink { desc: description } = &self.collections[&id].data_source else {
3555            return Err(StorageError::IdentifierMissing(id));
3556        };
3557
3558        let from_storage_metadata = self
3559            .storage_collections
3560            .collection_metadata(description.sink.from)?;
3561        let to_storage_metadata = self.storage_collections.collection_metadata(id)?;
3562
3563        // Choose an as-of frontier for this execution of the sink. If the write frontier of the sink
3564        // is strictly larger than its read hold, it must have at least written out its snapshot, and we can skip
3565        // reading it; otherwise assume we may have to replay from the beginning.
3566        let export_state = self.storage_collections.collection_frontiers(id)?;
3567        let mut as_of = description.sink.as_of.clone();
3568        as_of.join_assign(&export_state.implied_capability);
3569        let with_snapshot = description.sink.with_snapshot
3570            && !PartialOrder::less_than(&as_of, &export_state.write_frontier);
3571
3572        info!(
3573            sink_id = %id,
3574            from_id = %description.sink.from,
3575            write_frontier = ?export_state.write_frontier,
3576            ?as_of,
3577            ?with_snapshot,
3578            "run_export"
3579        );
3580
3581        let cmd = RunSinkCommand {
3582            id,
3583            description: StorageSinkDesc {
3584                from: description.sink.from,
3585                from_desc: description.sink.from_desc.clone(),
3586                connection: description.sink.connection.clone(),
3587                envelope: description.sink.envelope,
3588                as_of,
3589                version: description.sink.version,
3590                from_storage_metadata,
3591                with_snapshot,
3592                to_storage_metadata,
3593                commit_interval: description.sink.commit_interval,
3594            },
3595        };
3596
3597        let storage_instance_id = description.instance_id.clone();
3598
3599        let instance = self
3600            .instances
3601            .get_mut(&storage_instance_id)
3602            .ok_or_else(|| StorageError::ExportInstanceMissing {
3603                storage_instance_id,
3604                export_id: id,
3605            })?;
3606
3607        instance.send(StorageCommand::RunSink(Box::new(cmd)));
3608
3609        Ok(())
3610    }
3611
3612    /// Update introspection with the current frontiers of storage objects.
3613    ///
3614    /// This method is invoked by `Controller::maintain`, which we expect to be called once per
3615    /// second during normal operation.
3616    fn update_frontier_introspection(&mut self) {
3617        let mut global_frontiers = BTreeMap::new();
3618        let mut replica_frontiers = BTreeMap::new();
3619
3620        for collection_frontiers in self.storage_collections.active_collection_frontiers() {
3621            let id = collection_frontiers.id;
3622            let since = collection_frontiers.read_capabilities;
3623            let upper = collection_frontiers.write_frontier;
3624
3625            let instance = self
3626                .collections
3627                .get(&id)
3628                .and_then(|collection_state| match &collection_state.extra_state {
3629                    CollectionStateExtra::Ingestion(ingestion) => Some(ingestion.instance_id),
3630                    CollectionStateExtra::Export(export) => Some(export.cluster_id()),
3631                    CollectionStateExtra::None => None,
3632                })
3633                .and_then(|i| self.instances.get(&i));
3634
3635            if let Some(instance) = instance {
3636                for replica_id in instance.replica_ids() {
3637                    replica_frontiers.insert((id, replica_id), upper.clone());
3638                }
3639            }
3640
3641            global_frontiers.insert(id, (since, upper));
3642        }
3643
3644        let mut global_updates = Vec::new();
3645        let mut replica_updates = Vec::new();
3646
3647        let mut push_global_update =
3648            |id: GlobalId,
3649             (since, upper): (Antichain<Timestamp>, Antichain<Timestamp>),
3650             diff: Diff| {
3651                let read_frontier = since.into_option().map_or(Datum::Null, |t| t.into());
3652                let write_frontier = upper.into_option().map_or(Datum::Null, |t| t.into());
3653                let row = Row::pack_slice(&[
3654                    Datum::String(&id.to_string()),
3655                    read_frontier,
3656                    write_frontier,
3657                ]);
3658                global_updates.push((row, diff));
3659            };
3660
3661        let mut push_replica_update =
3662            |(id, replica_id): (GlobalId, ReplicaId), upper: Antichain<Timestamp>, diff: Diff| {
3663                let write_frontier = upper.into_option().map_or(Datum::Null, |t| t.into());
3664                let row = Row::pack_slice(&[
3665                    Datum::String(&id.to_string()),
3666                    Datum::String(&replica_id.to_string()),
3667                    write_frontier,
3668                ]);
3669                replica_updates.push((row, diff));
3670            };
3671
3672        let mut old_global_frontiers =
3673            std::mem::replace(&mut self.recorded_frontiers, global_frontiers);
3674        for (&id, new) in &self.recorded_frontiers {
3675            match old_global_frontiers.remove(&id) {
3676                Some(old) if &old != new => {
3677                    push_global_update(id, new.clone(), Diff::ONE);
3678                    push_global_update(id, old, Diff::MINUS_ONE);
3679                }
3680                Some(_) => (),
3681                None => push_global_update(id, new.clone(), Diff::ONE),
3682            }
3683        }
3684        for (id, old) in old_global_frontiers {
3685            push_global_update(id, old, Diff::MINUS_ONE);
3686        }
3687
3688        let mut old_replica_frontiers =
3689            std::mem::replace(&mut self.recorded_replica_frontiers, replica_frontiers);
3690        for (&key, new) in &self.recorded_replica_frontiers {
3691            match old_replica_frontiers.remove(&key) {
3692                Some(old) if &old != new => {
3693                    push_replica_update(key, new.clone(), Diff::ONE);
3694                    push_replica_update(key, old, Diff::MINUS_ONE);
3695                }
3696                Some(_) => (),
3697                None => push_replica_update(key, new.clone(), Diff::ONE),
3698            }
3699        }
3700        for (key, old) in old_replica_frontiers {
3701            push_replica_update(key, old, Diff::MINUS_ONE);
3702        }
3703
3704        let id = self.introspection_ids[&IntrospectionType::Frontiers];
3705        self.collection_manager
3706            .differential_append(id, global_updates);
3707
3708        let id = self.introspection_ids[&IntrospectionType::ReplicaFrontiers];
3709        self.collection_manager
3710            .differential_append(id, replica_updates);
3711    }
3712
3713    /// Refresh the wallclock lag introspection and metrics with the current lag values.
3714    ///
3715    /// This method produces wallclock lag metrics of two different shapes:
3716    ///
3717    /// * Histories: For each replica and each collection, we measure the lag of the write frontier
3718    ///   behind the wallclock time every second. Every minute we emit the maximum lag observed
3719    ///   over the last minute, together with the current time.
3720    /// * Histograms: For each collection, we measure the lag of the write frontier behind
3721    ///   wallclock time every second. Every minute we emit all lags observed over the last minute,
3722    ///   together with the current histogram period.
3723    ///
3724    /// Histories are emitted to both Mz introspection and Prometheus, histograms only to
3725    /// introspection. We treat lags of unreadable collections (i.e. collections that contain no
3726    /// readable times) as undefined and set them to NULL in introspection and `u64::MAX` in
3727    /// Prometheus.
3728    ///
3729    /// This method is invoked by `Controller::maintain`, which we expect to be called once per
3730    /// second during normal operation.
3731    fn refresh_wallclock_lag(&mut self) {
3732        let now_ms = (self.now)();
3733        let histogram_period =
3734            WallclockLagHistogramPeriod::from_epoch_millis(now_ms, self.config.config_set());
3735
3736        let frontier_lag = |frontier: &Antichain<Timestamp>| match frontier.as_option() {
3737            Some(ts) => (self.wallclock_lag)(*ts),
3738            None => Duration::ZERO,
3739        };
3740
3741        for frontiers in self.storage_collections.active_collection_frontiers() {
3742            let id = frontiers.id;
3743            let Some(collection) = self.collections.get_mut(&id) else {
3744                continue;
3745            };
3746
3747            let collection_unreadable =
3748                PartialOrder::less_equal(&frontiers.write_frontier, &frontiers.read_capabilities);
3749            let lag = if collection_unreadable {
3750                WallclockLag::Undefined
3751            } else {
3752                let lag = frontier_lag(&frontiers.write_frontier);
3753                WallclockLag::Seconds(lag.as_secs())
3754            };
3755
3756            collection.wallclock_lag_max = collection.wallclock_lag_max.max(lag);
3757
3758            // No way to specify values as undefined in Prometheus metrics, so we use the
3759            // maximum value instead.
3760            let secs = lag.unwrap_seconds_or(u64::MAX);
3761            collection.wallclock_lag_metrics.observe(secs);
3762
3763            if let Some(stash) = &mut collection.wallclock_lag_histogram_stash {
3764                let bucket = lag.map_seconds(|secs| secs.next_power_of_two());
3765
3766                let instance_id = match &collection.extra_state {
3767                    CollectionStateExtra::Ingestion(i) => Some(i.instance_id),
3768                    CollectionStateExtra::Export(e) => Some(e.cluster_id()),
3769                    CollectionStateExtra::None => None,
3770                };
3771                let workload_class = instance_id
3772                    .and_then(|id| self.instances.get(&id))
3773                    .and_then(|i| i.workload_class.clone());
3774                let labels = match workload_class {
3775                    Some(wc) => [("workload_class", wc.clone())].into(),
3776                    None => BTreeMap::new(),
3777                };
3778
3779                let key = (histogram_period, bucket, labels);
3780                *stash.entry(key).or_default() += Diff::ONE;
3781            }
3782        }
3783
3784        // Record lags to persist, if it's time.
3785        self.maybe_record_wallclock_lag();
3786    }
3787
3788    /// Produce new wallclock lag introspection updates, provided enough time has passed since the
3789    /// last recording.
3790    ///
3791    /// We emit new introspection updates if the system time has passed into a new multiple of the
3792    /// recording interval (typically 1 minute) since the last refresh. The compute controller uses
3793    /// the same approach, ensuring that both controllers commit their lags at roughly the same
3794    /// time, avoiding confusion caused by inconsistencies.
3795    fn maybe_record_wallclock_lag(&mut self) {
3796        if self.read_only {
3797            return;
3798        }
3799
3800        let duration_trunc = |datetime: DateTime<_>, interval| {
3801            let td = TimeDelta::from_std(interval).ok()?;
3802            datetime.duration_trunc(td).ok()
3803        };
3804
3805        let interval = WALLCLOCK_LAG_RECORDING_INTERVAL.get(self.config.config_set());
3806        let now_dt = mz_ore::now::to_datetime((self.now)());
3807        let now_trunc = duration_trunc(now_dt, interval).unwrap_or_else(|| {
3808            soft_panic_or_log!("excessive wallclock lag recording interval: {interval:?}");
3809            let default = WALLCLOCK_LAG_RECORDING_INTERVAL.default();
3810            duration_trunc(now_dt, *default).unwrap()
3811        });
3812        if now_trunc <= self.wallclock_lag_last_recorded {
3813            return;
3814        }
3815
3816        let now_ts: CheckedTimestamp<_> = now_trunc.try_into().expect("must fit");
3817
3818        let mut history_updates = Vec::new();
3819        let mut histogram_updates = Vec::new();
3820        let mut row_buf = Row::default();
3821        for frontiers in self.storage_collections.active_collection_frontiers() {
3822            let id = frontiers.id;
3823            let Some(collection) = self.collections.get_mut(&id) else {
3824                continue;
3825            };
3826
3827            let max_lag = std::mem::replace(&mut collection.wallclock_lag_max, WallclockLag::MIN);
3828            let row = Row::pack_slice(&[
3829                Datum::String(&id.to_string()),
3830                Datum::Null,
3831                max_lag.into_interval_datum(),
3832                Datum::TimestampTz(now_ts),
3833            ]);
3834            history_updates.push((row, Diff::ONE));
3835
3836            let Some(stash) = &mut collection.wallclock_lag_histogram_stash else {
3837                continue;
3838            };
3839
3840            for ((period, lag, labels), count) in std::mem::take(stash) {
3841                let mut packer = row_buf.packer();
3842                packer.extend([
3843                    Datum::TimestampTz(period.start),
3844                    Datum::TimestampTz(period.end),
3845                    Datum::String(&id.to_string()),
3846                    lag.into_uint64_datum(),
3847                ]);
3848                let labels = labels.iter().map(|(k, v)| (*k, Datum::String(v)));
3849                packer.push_dict(labels);
3850
3851                histogram_updates.push((row_buf.clone(), count));
3852            }
3853        }
3854
3855        if !history_updates.is_empty() {
3856            self.append_introspection_updates(
3857                IntrospectionType::WallclockLagHistory,
3858                history_updates,
3859            );
3860        }
3861        if !histogram_updates.is_empty() {
3862            self.append_introspection_updates(
3863                IntrospectionType::WallclockLagHistogram,
3864                histogram_updates,
3865            );
3866        }
3867
3868        self.wallclock_lag_last_recorded = now_trunc;
3869    }
3870
3871    /// Run periodic tasks.
3872    ///
3873    /// This method is invoked roughly once per second during normal operation. It is a good place
3874    /// for tasks that need to run periodically, such as state cleanup or updating of metrics.
3875    fn maintain(&mut self) {
3876        self.update_frontier_introspection();
3877        self.refresh_wallclock_lag();
3878
3879        // Perform instance maintenance work.
3880        for instance in self.instances.values_mut() {
3881            instance.refresh_state_metrics();
3882        }
3883    }
3884}
3885
3886impl From<&IntrospectionType> for CollectionManagerKind {
3887    fn from(value: &IntrospectionType) -> Self {
3888        match value {
3889            IntrospectionType::ShardMapping
3890            | IntrospectionType::Frontiers
3891            | IntrospectionType::ReplicaFrontiers
3892            | IntrospectionType::StorageSourceStatistics
3893            | IntrospectionType::StorageSinkStatistics
3894            | IntrospectionType::ComputeDependencies
3895            | IntrospectionType::ComputeOperatorHydrationStatus
3896            | IntrospectionType::ComputeMaterializedViewRefreshes
3897            | IntrospectionType::ComputeErrorCounts
3898            | IntrospectionType::ComputeHydrationTimes
3899            | IntrospectionType::ComputeObjectArrangementSizes => {
3900                CollectionManagerKind::Differential
3901            }
3902
3903            IntrospectionType::SourceStatusHistory
3904            | IntrospectionType::SinkStatusHistory
3905            | IntrospectionType::PrivatelinkConnectionStatusHistory
3906            | IntrospectionType::ReplicaStatusHistory
3907            | IntrospectionType::ReplicaMetricsHistory
3908            | IntrospectionType::WallclockLagHistory
3909            | IntrospectionType::WallclockLagHistogram
3910            | IntrospectionType::PreparedStatementHistory
3911            | IntrospectionType::StatementExecutionHistory
3912            | IntrospectionType::SessionHistory
3913            | IntrospectionType::StatementLifecycleHistory
3914            | IntrospectionType::SqlText => CollectionManagerKind::AppendOnly,
3915        }
3916    }
3917}
3918
3919/// Get the current rows in the given statistics table. This is used to bootstrap
3920/// the statistics tasks.
3921///
3922// TODO(guswynn): we need to be more careful about the update time we get here:
3923// <https://github.com/MaterializeInc/database-issues/issues/7564>
3924async fn snapshot_statistics(
3925    id: GlobalId,
3926    upper: Antichain<Timestamp>,
3927    storage_collections: &Arc<dyn StorageCollections + Send + Sync>,
3928) -> Vec<Row> {
3929    match upper.as_option() {
3930        Some(f) if f > &Timestamp::MIN => {
3931            let as_of = f.step_back().unwrap();
3932
3933            let snapshot = storage_collections.snapshot(id, as_of).await.unwrap();
3934            snapshot
3935                .into_iter()
3936                .map(|(row, diff)| {
3937                    assert_eq!(diff, 1);
3938                    row
3939                })
3940                .collect()
3941        }
3942        // If collection is closed or the frontier is the minimum, we cannot
3943        // or don't need to truncate (respectively).
3944        _ => Vec::new(),
3945    }
3946}
3947
3948async fn read_handle_for_snapshot(
3949    persist: &PersistClientCache,
3950    id: GlobalId,
3951    metadata: &CollectionMetadata,
3952) -> Result<ReadHandle<SourceData, (), Timestamp, StorageDiff>, StorageError> {
3953    let persist_client = persist
3954        .open(metadata.persist_location.clone())
3955        .await
3956        .unwrap();
3957
3958    // We create a new read handle every time someone requests a snapshot and then immediately
3959    // expire it instead of keeping a read handle permanently in our state to avoid having it
3960    // heartbeat continously. The assumption is that calls to snapshot are rare and therefore
3961    // worth it to always create a new handle.
3962    let read_handle = persist_client
3963        .open_leased_reader::<SourceData, (), _, _>(
3964            metadata.data_shard,
3965            Arc::new(metadata.relation_desc.clone()),
3966            Arc::new(UnitSchema),
3967            Diagnostics {
3968                shard_name: id.to_string(),
3969                handle_purpose: format!("snapshot {}", id),
3970            },
3971            USE_CRITICAL_SINCE_SNAPSHOT.get(persist_client.dyncfgs()),
3972        )
3973        .await
3974        .expect("invalid persist usage");
3975    Ok(read_handle)
3976}
3977
3978/// State maintained about individual collections.
3979#[derive(Debug)]
3980struct CollectionState {
3981    /// The source of this collection's data.
3982    pub data_source: DataSource,
3983
3984    pub collection_metadata: CollectionMetadata,
3985
3986    pub extra_state: CollectionStateExtra,
3987
3988    /// Maximum frontier wallclock lag since the last `WallclockLagHistory` introspection update.
3989    wallclock_lag_max: WallclockLag,
3990    /// Frontier wallclock lag measurements stashed until the next `WallclockLagHistogram`
3991    /// introspection update.
3992    ///
3993    /// Keys are `(period, lag, labels)` triples, values are counts.
3994    ///
3995    /// If this is `None`, wallclock lag is not tracked for this collection.
3996    wallclock_lag_histogram_stash: Option<
3997        BTreeMap<
3998            (
3999                WallclockLagHistogramPeriod,
4000                WallclockLag,
4001                BTreeMap<&'static str, String>,
4002            ),
4003            Diff,
4004        >,
4005    >,
4006    /// Frontier wallclock lag metrics tracked for this collection.
4007    wallclock_lag_metrics: WallclockLagMetrics,
4008}
4009
4010impl CollectionState {
4011    fn new(
4012        data_source: DataSource,
4013        collection_metadata: CollectionMetadata,
4014        extra_state: CollectionStateExtra,
4015        wallclock_lag_metrics: WallclockLagMetrics,
4016    ) -> Self {
4017        // Only collect wallclock lag histogram data for collections written by storage, to avoid
4018        // duplicate measurements. Collections written by other components (e.g. compute) have
4019        // their wallclock lags recorded by these components.
4020        let wallclock_lag_histogram_stash = match &data_source {
4021            DataSource::Other => None,
4022            _ => Some(Default::default()),
4023        };
4024
4025        Self {
4026            data_source,
4027            collection_metadata,
4028            extra_state,
4029            wallclock_lag_max: WallclockLag::MIN,
4030            wallclock_lag_histogram_stash,
4031            wallclock_lag_metrics,
4032        }
4033    }
4034}
4035
4036/// Additional state that the controller maintains for select collection types.
4037#[derive(Debug)]
4038enum CollectionStateExtra {
4039    Ingestion(IngestionState),
4040    Export(ExportState),
4041    None,
4042}
4043
4044/// State maintained about ingestions and ingestion exports
4045#[derive(Debug)]
4046struct IngestionState {
4047    /// Really only for keeping track of changes to the `derived_since`.
4048    pub read_capabilities: MutableAntichain<Timestamp>,
4049
4050    /// The current since frontier, derived from `write_frontier` using
4051    /// `hold_policy`.
4052    pub derived_since: Antichain<Timestamp>,
4053
4054    /// Holds that this ingestion (or ingestion export) has on its dependencies.
4055    pub dependency_read_holds: Vec<ReadHold>,
4056
4057    /// Reported write frontier.
4058    pub write_frontier: Antichain<Timestamp>,
4059
4060    /// The policy that drives how we downgrade our read hold. That is how we
4061    /// derive our since from our upper.
4062    ///
4063    /// This is a _storage-controller-internal_ policy used to derive its
4064    /// personal read hold on the collection. It should not be confused with any
4065    /// read policies that the adapter might install at [StorageCollections].
4066    pub hold_policy: ReadPolicy,
4067
4068    /// The ID of the instance in which the ingestion is running.
4069    pub instance_id: StorageInstanceId,
4070
4071    /// Set of replica IDs on which this ingestion is hydrated.
4072    pub hydrated_on: BTreeSet<ReplicaId>,
4073}
4074
4075/// A description of a status history collection.
4076///
4077/// Used to inform partial truncation, see
4078/// [`collection_mgmt::partially_truncate_status_history`].
4079struct StatusHistoryDesc<K> {
4080    retention_policy: StatusHistoryRetentionPolicy,
4081    extract_key: Box<dyn Fn(&[Datum]) -> K + Send>,
4082    extract_time: Box<dyn Fn(&[Datum]) -> CheckedTimestamp<DateTime<Utc>> + Send>,
4083}
4084enum StatusHistoryRetentionPolicy {
4085    // Truncates everything but the last N updates for each key.
4086    LastN(usize),
4087    // Truncates everything past the time window for each key.
4088    TimeWindow(Duration),
4089}
4090
4091fn source_status_history_desc(
4092    params: &StorageParameters,
4093) -> StatusHistoryDesc<(GlobalId, Option<ReplicaId>)> {
4094    let desc = &MZ_SOURCE_STATUS_HISTORY_DESC;
4095    let (source_id_idx, _) = desc.get_by_name(&"source_id".into()).expect("exists");
4096    let (replica_id_idx, _) = desc.get_by_name(&"replica_id".into()).expect("exists");
4097    let (time_idx, _) = desc.get_by_name(&"occurred_at".into()).expect("exists");
4098
4099    StatusHistoryDesc {
4100        retention_policy: StatusHistoryRetentionPolicy::LastN(
4101            params.keep_n_source_status_history_entries,
4102        ),
4103        extract_key: Box::new(move |datums| {
4104            (
4105                GlobalId::from_str(datums[source_id_idx].unwrap_str()).expect("GlobalId column"),
4106                if datums[replica_id_idx].is_null() {
4107                    None
4108                } else {
4109                    Some(
4110                        ReplicaId::from_str(datums[replica_id_idx].unwrap_str())
4111                            .expect("ReplicaId column"),
4112                    )
4113                },
4114            )
4115        }),
4116        extract_time: Box::new(move |datums| datums[time_idx].unwrap_timestamptz()),
4117    }
4118}
4119
4120fn sink_status_history_desc(
4121    params: &StorageParameters,
4122) -> StatusHistoryDesc<(GlobalId, Option<ReplicaId>)> {
4123    let desc = &MZ_SINK_STATUS_HISTORY_DESC;
4124    let (sink_id_idx, _) = desc.get_by_name(&"sink_id".into()).expect("exists");
4125    let (replica_id_idx, _) = desc.get_by_name(&"replica_id".into()).expect("exists");
4126    let (time_idx, _) = desc.get_by_name(&"occurred_at".into()).expect("exists");
4127
4128    StatusHistoryDesc {
4129        retention_policy: StatusHistoryRetentionPolicy::LastN(
4130            params.keep_n_sink_status_history_entries,
4131        ),
4132        extract_key: Box::new(move |datums| {
4133            (
4134                GlobalId::from_str(datums[sink_id_idx].unwrap_str()).expect("GlobalId column"),
4135                if datums[replica_id_idx].is_null() {
4136                    None
4137                } else {
4138                    Some(
4139                        ReplicaId::from_str(datums[replica_id_idx].unwrap_str())
4140                            .expect("ReplicaId column"),
4141                    )
4142                },
4143            )
4144        }),
4145        extract_time: Box::new(move |datums| datums[time_idx].unwrap_timestamptz()),
4146    }
4147}
4148
4149fn privatelink_status_history_desc(params: &StorageParameters) -> StatusHistoryDesc<GlobalId> {
4150    let desc = &MZ_AWS_PRIVATELINK_CONNECTION_STATUS_HISTORY_DESC;
4151    let (key_idx, _) = desc.get_by_name(&"connection_id".into()).expect("exists");
4152    let (time_idx, _) = desc.get_by_name(&"occurred_at".into()).expect("exists");
4153
4154    StatusHistoryDesc {
4155        retention_policy: StatusHistoryRetentionPolicy::LastN(
4156            params.keep_n_privatelink_status_history_entries,
4157        ),
4158        extract_key: Box::new(move |datums| {
4159            GlobalId::from_str(datums[key_idx].unwrap_str()).expect("GlobalId column")
4160        }),
4161        extract_time: Box::new(move |datums| datums[time_idx].unwrap_timestamptz()),
4162    }
4163}
4164
4165fn replica_status_history_desc(params: &StorageParameters) -> StatusHistoryDesc<(GlobalId, u64)> {
4166    let desc = &REPLICA_STATUS_HISTORY_DESC;
4167    let (replica_idx, _) = desc.get_by_name(&"replica_id".into()).expect("exists");
4168    let (process_idx, _) = desc.get_by_name(&"process_id".into()).expect("exists");
4169    let (time_idx, _) = desc.get_by_name(&"occurred_at".into()).expect("exists");
4170
4171    StatusHistoryDesc {
4172        retention_policy: StatusHistoryRetentionPolicy::TimeWindow(
4173            params.replica_status_history_retention_window,
4174        ),
4175        extract_key: Box::new(move |datums| {
4176            (
4177                GlobalId::from_str(datums[replica_idx].unwrap_str()).expect("GlobalId column"),
4178                datums[process_idx].unwrap_uint64(),
4179            )
4180        }),
4181        extract_time: Box::new(move |datums| datums[time_idx].unwrap_timestamptz()),
4182    }
4183}
4184
4185/// Replace one antichain with another, tracking the overall changes in the returned `ChangeBatch`.
4186fn swap_updates(
4187    from: &mut Antichain<Timestamp>,
4188    mut replace_with: Antichain<Timestamp>,
4189) -> ChangeBatch<Timestamp> {
4190    let mut update = ChangeBatch::new();
4191    if PartialOrder::less_equal(from, &replace_with) {
4192        update.extend(replace_with.iter().map(|time| (*time, 1)));
4193        std::mem::swap(from, &mut replace_with);
4194        update.extend(replace_with.iter().map(|time| (*time, -1)));
4195    }
4196    update
4197}