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