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