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