Skip to main content

mz_storage_client/
storage_collections.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//! An abstraction for dealing with storage collections.
11
12use std::cmp::Reverse;
13use std::collections::{BTreeMap, BTreeSet};
14use std::fmt::Debug;
15use std::iter;
16use std::num::NonZeroI64;
17use std::sync::{Arc, Mutex};
18use std::time::Duration;
19
20use async_trait::async_trait;
21use differential_dataflow::lattice::Lattice;
22use futures::future::BoxFuture;
23use futures::stream::{BoxStream, FuturesUnordered};
24use futures::{Future, FutureExt, StreamExt};
25use itertools::Itertools;
26use mz_ore::collections::CollectionExt;
27use mz_ore::metrics::MetricsRegistry;
28use mz_ore::now::NowFn;
29use mz_ore::task::AbortOnDropHandle;
30use mz_ore::{assert_none, instrument, soft_assert_or_log};
31use mz_persist_client::cache::PersistClientCache;
32use mz_persist_client::cfg::USE_CRITICAL_SINCE_SNAPSHOT;
33use mz_persist_client::critical::{Opaque, SinceHandle};
34use mz_persist_client::read::{Cursor, ReadHandle};
35use mz_persist_client::schema::CaESchema;
36use mz_persist_client::stats::{SnapshotPartsStats, SnapshotStats};
37use mz_persist_client::write::WriteHandle;
38use mz_persist_client::{Diagnostics, PersistClient, PersistLocation, ShardId};
39use mz_persist_types::codec_impls::UnitSchema;
40use mz_persist_types::txn::TxnsCodec;
41use mz_repr::{GlobalId, RelationDesc, RelationVersion, Row, Timestamp};
42use mz_storage_types::StorageDiff;
43use mz_storage_types::configuration::StorageConfiguration;
44use mz_storage_types::connections::ConnectionContext;
45use mz_storage_types::controller::{CollectionMetadata, StorageError, TxnsCodecRow};
46use mz_storage_types::dyncfgs::STORAGE_DOWNGRADE_SINCE_DURING_FINALIZATION;
47use mz_storage_types::errors::CollectionMissing;
48use mz_storage_types::parameters::StorageParameters;
49use mz_storage_types::read_holds::ReadHold;
50use mz_storage_types::read_policy::ReadPolicy;
51use mz_storage_types::sources::{GenericSourceConnection, SourceData, SourceEnvelope, Timeline};
52use mz_storage_types::time_dependence::{TimeDependence, TimeDependenceError};
53use mz_txn_wal::metrics::Metrics as TxnMetrics;
54use mz_txn_wal::txn_read::{DataSnapshot, TxnsRead};
55use mz_txn_wal::txns::TxnsHandle;
56use timely::PartialOrder;
57use timely::progress::frontier::MutableAntichain;
58use timely::progress::{Antichain, ChangeBatch};
59use tokio::sync::{mpsc, oneshot};
60use tokio::time::MissedTickBehavior;
61use tracing::{debug, info, trace, warn};
62
63use crate::client::TimestamplessUpdateBuilder;
64use crate::controller::{
65    CollectionDescription, DataSource, PersistEpoch, StorageMetadata, StorageTxn,
66};
67use crate::storage_collections::metrics::{ShardIdSet, StorageCollectionsMetrics};
68
69mod metrics;
70
71/// An abstraction for keeping track of storage collections and managing access
72/// to them.
73///
74/// Responsibilities:
75///
76/// - Keeps a critical persist handle for holding the since of collections
77///   where it need to be.
78///
79/// - Drives the since forward based on the upper of a collection and a
80///   [ReadPolicy].
81///
82/// - Hands out [ReadHolds](ReadHold) that prevent a collection's since from
83/// advancing while it needs to be read at a specific time.
84#[async_trait]
85pub trait StorageCollections: Debug + Sync {
86    /// On boot, reconcile this [StorageCollections] with outside state. We get
87    /// a [StorageTxn] where we can record any durable state that we need.
88    ///
89    /// We get `init_ids`, which tells us about all collections that currently
90    /// exist, so that we can record durable state for those that _we_ don't
91    /// know yet about.
92    async fn initialize_state(
93        &self,
94        txn: &mut (dyn StorageTxn + Send),
95        init_ids: BTreeSet<GlobalId>,
96    ) -> Result<(), StorageError>;
97
98    /// Update storage configuration with new parameters.
99    fn update_parameters(&self, config_params: StorageParameters);
100
101    /// Returns the [CollectionMetadata] of the collection identified by `id`.
102    fn collection_metadata(&self, id: GlobalId) -> Result<CollectionMetadata, CollectionMissing>;
103
104    /// Acquire an iterator over [CollectionMetadata] for all active
105    /// collections.
106    ///
107    /// A collection is "active" when it has a non empty frontier of read
108    /// capabilties.
109    fn active_collection_metadatas(&self) -> Vec<(GlobalId, CollectionMetadata)>;
110
111    /// Returns the frontiers of the identified collection.
112    fn collection_frontiers(&self, id: GlobalId) -> Result<CollectionFrontiers, CollectionMissing> {
113        let frontiers = self
114            .collections_frontiers(vec![id])?
115            .expect_element(|| "known to exist");
116
117        Ok(frontiers)
118    }
119
120    /// Atomically gets and returns the frontiers of all the identified
121    /// collections.
122    fn collections_frontiers(
123        &self,
124        id: Vec<GlobalId>,
125    ) -> Result<Vec<CollectionFrontiers>, CollectionMissing>;
126
127    /// Atomically gets and returns the frontiers of all active collections.
128    ///
129    /// A collection is "active" when it has a non-empty frontier of read
130    /// capabilities.
131    fn active_collection_frontiers(&self) -> Vec<CollectionFrontiers>;
132
133    /// Checks whether a collection exists under the given `GlobalId`. Returns
134    /// an error if the collection does not exist.
135    fn check_exists(&self, id: GlobalId) -> Result<(), StorageError>;
136
137    /// Returns aggregate statistics about the contents of the local input named
138    /// `id` at `as_of`.
139    async fn snapshot_stats(
140        &self,
141        id: GlobalId,
142        as_of: Antichain<Timestamp>,
143    ) -> Result<SnapshotStats, StorageError>;
144
145    /// Returns aggregate statistics about the contents of the local input named
146    /// `id` at `as_of`.
147    ///
148    /// Note that this async function itself returns a future. We may
149    /// need to block on the stats being available, but don't want to hold a reference
150    /// to the controller for too long... so the outer future holds a reference to the
151    /// controller but returns quickly, and the inner future is slow but does not
152    /// reference the controller.
153    async fn snapshot_parts_stats(
154        &self,
155        id: GlobalId,
156        as_of: Antichain<Timestamp>,
157    ) -> BoxFuture<'static, Result<SnapshotPartsStats, StorageError>>;
158
159    /// Returns a snapshot of the contents of collection `id` at `as_of`.
160    fn snapshot(
161        &self,
162        id: GlobalId,
163        as_of: Timestamp,
164    ) -> BoxFuture<'static, Result<Vec<(Row, StorageDiff)>, StorageError>>;
165
166    /// Returns a snapshot of the contents of collection `id` at the largest readable `as_of`.
167    /// The collection must consolidate to a set, i.e., the multiplicity of every row must be 1!
168    ///
169    /// # Errors
170    ///
171    /// - Returns `StorageError::InvalidUsage` if the collection is closed.
172    /// - Propagates the error if the underlying `snapshot` call errors.
173    ///
174    /// # Panics
175    ///
176    /// Panics if the collection does not consolidate to a set at that `as_of`
177    /// (i.e., if any row survives with a multiplicity other than `+1`). Only
178    /// safe to call on collections whose producer guarantees set semantics;
179    /// not safe on arbitrary user collections.
180    async fn snapshot_latest(&self, id: GlobalId) -> Result<Vec<Row>, StorageError>;
181
182    /// Returns a snapshot of the contents of collection `id` at `as_of`.
183    fn snapshot_cursor(
184        &self,
185        id: GlobalId,
186        as_of: Timestamp,
187    ) -> BoxFuture<'static, Result<SnapshotCursor, StorageError>>;
188
189    /// Generates a snapshot of the contents of collection `id` at `as_of` and
190    /// streams out all of the updates in bounded memory.
191    ///
192    /// The output is __not__ consolidated.
193    fn snapshot_and_stream(
194        &self,
195        id: GlobalId,
196        as_of: Timestamp,
197    ) -> BoxFuture<
198        'static,
199        Result<BoxStream<'static, (SourceData, Timestamp, StorageDiff)>, StorageError>,
200    >;
201
202    /// Create a [`TimestamplessUpdateBuilder`] that can be used to stage
203    /// updates for the provided [`GlobalId`].
204    fn create_update_builder(
205        &self,
206        id: GlobalId,
207    ) -> BoxFuture<
208        'static,
209        Result<TimestamplessUpdateBuilder<SourceData, (), StorageDiff>, StorageError>,
210    >;
211
212    /// Update the given [`StorageTxn`] with the appropriate metadata given the
213    /// IDs to add and drop.
214    ///
215    /// The data modified in the `StorageTxn` must be made available in all
216    /// subsequent calls that require [`StorageMetadata`] as a parameter.
217    async fn prepare_state(
218        &self,
219        txn: &mut (dyn StorageTxn + Send),
220        ids_to_add: BTreeSet<GlobalId>,
221        ids_to_drop: BTreeSet<GlobalId>,
222        ids_to_register: BTreeMap<GlobalId, ShardId>,
223    ) -> Result<(), StorageError>;
224
225    /// Create the collections described by the individual
226    /// [CollectionDescriptions](CollectionDescription).
227    ///
228    /// Each command carries the source id, the source description, and any
229    /// associated metadata needed to ingest the particular source.
230    ///
231    /// This command installs collection state for the indicated sources, and
232    /// they are now valid to use in queries at times beyond the initial `since`
233    /// frontiers. Each collection also acquires a read capability at this
234    /// frontier, which will need to be repeatedly downgraded with
235    /// `allow_compaction()` to permit compaction.
236    ///
237    /// This method is NOT idempotent; It can fail between processing of
238    /// different collections and leave the [StorageCollections] in an
239    /// inconsistent state. It is almost always wrong to do anything but abort
240    /// the process on `Err`.
241    ///
242    /// The `register_ts` is used as the initial timestamp that tables are
243    /// available for reads. (We might later give non-tables the same treatment,
244    /// but hold off on that initially.) Callers must provide a Some if any of
245    /// the collections is a table. A None may be given if none of the
246    /// collections are a table (i.e. all materialized views, sources, etc).
247    ///
248    /// `migrated_storage_collections` is a set of migrated storage collections to be excluded
249    /// from the txn-wal sub-system.
250    async fn create_collections_for_bootstrap(
251        &self,
252        storage_metadata: &StorageMetadata,
253        register_ts: Option<Timestamp>,
254        collections: Vec<(GlobalId, CollectionDescription)>,
255        migrated_storage_collections: &BTreeSet<GlobalId>,
256    ) -> Result<(), StorageError>;
257
258    /// Updates the [`RelationDesc`] for the specified table.
259    async fn alter_table_desc(
260        &self,
261        existing_collection: GlobalId,
262        new_collection: GlobalId,
263        new_desc: RelationDesc,
264        expected_version: RelationVersion,
265    ) -> Result<(), StorageError>;
266
267    /// Drops the read capability for the sources and allows their resources to
268    /// be reclaimed.
269    ///
270    /// TODO(jkosh44): This method does not validate the provided identifiers.
271    /// Currently when the controller starts/restarts it has no durable state.
272    /// That means that it has no way of remembering any past commands sent. In
273    /// the future we plan on persisting state for the controller so that it is
274    /// aware of past commands. Therefore this method is for dropping sources
275    /// that we know to have been previously created, but have been forgotten by
276    /// the controller due to a restart. Once command history becomes durable we
277    /// can remove this method and use the normal `drop_sources`.
278    fn drop_collections_unvalidated(
279        &self,
280        storage_metadata: &StorageMetadata,
281        identifiers: Vec<GlobalId>,
282    );
283
284    /// Assigns a read policy to specific identifiers.
285    ///
286    /// The policies are assigned in the order presented, and repeated
287    /// identifiers should conclude with the last policy. Changing a policy will
288    /// immediately downgrade the read capability if appropriate, but it will
289    /// not "recover" the read capability if the prior capability is already
290    /// ahead of it.
291    ///
292    /// This [StorageCollections] may include its own overrides on these
293    /// policies.
294    ///
295    /// Identifiers not present in `policies` retain their existing read
296    /// policies.
297    fn set_read_policies(&self, policies: Vec<(GlobalId, ReadPolicy)>);
298
299    /// Acquires and returns the earliest possible read holds for the specified
300    /// collections.
301    fn acquire_read_holds(
302        &self,
303        desired_holds: Vec<GlobalId>,
304    ) -> Result<Vec<ReadHold>, CollectionMissing>;
305
306    /// Get the time dependence for a storage collection. Returns no value if unknown or if
307    /// the object isn't managed by storage.
308    fn determine_time_dependence(
309        &self,
310        id: GlobalId,
311    ) -> Result<Option<TimeDependence>, TimeDependenceError>;
312
313    /// Returns the state of [`StorageCollections`] formatted as JSON.
314    fn dump(&self) -> Result<serde_json::Value, anyhow::Error>;
315}
316
317/// A cursor over a snapshot, allowing us to read just part of a snapshot in its
318/// consolidated form.
319pub struct SnapshotCursor {
320    // We allocate a temporary read handle for each snapshot, and that handle needs to live at
321    // least as long as the cursor itself, which holds part leases. Bundling them together!
322    pub _read_handle: ReadHandle<SourceData, (), Timestamp, StorageDiff>,
323    pub cursor: Cursor<SourceData, (), Timestamp, StorageDiff>,
324}
325
326impl SnapshotCursor {
327    pub async fn next(
328        &mut self,
329    ) -> Option<impl Iterator<Item = (SourceData, Timestamp, StorageDiff)> + Sized + '_> {
330        let iter = self.cursor.next().await?;
331        Some(iter.map(|((k, ()), t, d)| (k, t, d)))
332    }
333}
334
335/// Frontiers of the collection identified by `id`.
336#[derive(Debug)]
337pub struct CollectionFrontiers {
338    /// The [GlobalId] of the collection that these frontiers belong to.
339    pub id: GlobalId,
340
341    /// The upper/write frontier of the collection.
342    pub write_frontier: Antichain<Timestamp>,
343
344    /// The since frontier that is implied by the collection's existence,
345    /// disregarding any read holds.
346    ///
347    /// Concretely, it is the since frontier that is implied by the combination
348    /// of the `write_frontier` and a [ReadPolicy]. The implied capability is
349    /// derived from the write frontier using the [ReadPolicy].
350    pub implied_capability: Antichain<Timestamp>,
351
352    /// The frontier of all oustanding [ReadHolds](ReadHold). This includes the
353    /// implied capability.
354    pub read_capabilities: Antichain<Timestamp>,
355}
356
357/// Implementation of [StorageCollections] that is shallow-cloneable and uses a
358/// background task for doing work concurrently, in the background.
359#[derive(Debug, Clone)]
360pub struct StorageCollectionsImpl {
361    /// The fencing token for this instance of [StorageCollections], and really
362    /// all of the controllers and Coordinator.
363    envd_epoch: NonZeroI64,
364
365    /// Whether or not this [StorageCollections] is in read-only mode.
366    ///
367    /// When in read-only mode, we are not allowed to affect changes to external
368    /// systems, including, for example, acquiring and downgrading critical
369    /// [SinceHandles](SinceHandle)
370    read_only: bool,
371
372    /// The set of [ShardIds](ShardId) that we have to finalize. These will have
373    /// been persisted by the caller of [StorageCollections::prepare_state].
374    finalizable_shards: Arc<ShardIdSet>,
375
376    /// The set of [ShardIds](ShardId) that we have finalized. We keep track of
377    /// shards here until we are given a chance to let our callers know that
378    /// these have been finalized, for example via
379    /// [StorageCollections::prepare_state].
380    finalized_shards: Arc<ShardIdSet>,
381
382    /// Collections maintained by this [StorageCollections].
383    collections: Arc<std::sync::Mutex<BTreeMap<GlobalId, CollectionState>>>,
384
385    /// A shared TxnsCache running in a task and communicated with over a channel.
386    txns_read: TxnsRead<Timestamp>,
387
388    /// Storage configuration parameters.
389    config: Arc<Mutex<StorageConfiguration>>,
390
391    /// The upper of the txn shard as it was when we booted. We forward the
392    /// upper of created/registered tables to make sure that their uppers are
393    /// not less than the initially known txn upper.
394    ///
395    /// NOTE: This works around a quirk in how the adapter chooses the as_of of
396    /// existing indexes when bootstrapping, where tables that have an upper
397    /// that is less than the initially known txn upper can lead to indexes that
398    /// cannot hydrate in read-only mode.
399    initial_txn_upper: Antichain<Timestamp>,
400
401    /// The persist location where all storage collections are being written to
402    persist_location: PersistLocation,
403
404    /// A persist client used to write to storage collections
405    persist: Arc<PersistClientCache>,
406
407    /// For sending commands to our internal task.
408    cmd_tx: mpsc::UnboundedSender<BackgroundCmd>,
409
410    /// For sending updates about read holds to our internal task.
411    holds_tx: mpsc::UnboundedSender<(GlobalId, ChangeBatch<Timestamp>)>,
412
413    /// Handles to tasks we own, making sure they're dropped when we are.
414    _background_task: Arc<AbortOnDropHandle<()>>,
415    _finalize_shards_task: Arc<AbortOnDropHandle<()>>,
416}
417
418// Supporting methods for implementing [StorageCollections].
419//
420// Almost all internal methods that are the backing implementation for a trait
421// method have the `_inner` suffix.
422//
423// We follow a pattern where `_inner` methods get a mutable reference to the
424// shared collections state, and it's the public-facing method that locks the
425/// A boxed stream of source data with timestamps and diffs.
426type SourceDataStream = BoxStream<'static, (SourceData, Timestamp, StorageDiff)>;
427
428// state for the duration of its invocation. This allows calling other `_inner`
429// methods from within `_inner` methods.
430impl StorageCollectionsImpl {
431    /// Creates and returns a new [StorageCollections].
432    ///
433    /// Note that when creating a new [StorageCollections], you must also
434    /// reconcile it with the previous state using
435    /// [StorageCollections::initialize_state],
436    /// [StorageCollections::prepare_state], and
437    /// [StorageCollections::create_collections_for_bootstrap].
438    pub async fn new(
439        persist_location: PersistLocation,
440        persist_clients: Arc<PersistClientCache>,
441        metrics_registry: &MetricsRegistry,
442        _now: NowFn,
443        txns_metrics: Arc<TxnMetrics>,
444        envd_epoch: NonZeroI64,
445        read_only: bool,
446        connection_context: ConnectionContext,
447        txn: &dyn StorageTxn,
448    ) -> Self {
449        let metrics = StorageCollectionsMetrics::register_into(metrics_registry);
450
451        // This value must be already installed because we must ensure it's
452        // durably recorded before it is used, otherwise we risk leaking persist
453        // state.
454        let txns_id = txn
455            .get_txn_wal_shard()
456            .expect("must call prepare initialization before creating StorageCollections");
457
458        let txns_client = persist_clients
459            .open(persist_location.clone())
460            .await
461            .expect("location should be valid");
462
463        // We have to initialize, so that TxnsRead::start() below does not
464        // block.
465        let _txns_handle: TxnsHandle<SourceData, (), Timestamp, StorageDiff, TxnsCodecRow> =
466            TxnsHandle::open(
467                Timestamp::MIN,
468                txns_client.clone(),
469                txns_client.dyncfgs().clone(),
470                Arc::clone(&txns_metrics),
471                txns_id,
472                Opaque::encode(&PersistEpoch::default()),
473            )
474            .await;
475
476        // For handing to the background task, for listening to upper updates.
477        let (txns_key_schema, txns_val_schema) = TxnsCodecRow::schemas();
478        let mut txns_write = txns_client
479            .open_writer(
480                txns_id,
481                Arc::new(txns_key_schema),
482                Arc::new(txns_val_schema),
483                Diagnostics {
484                    shard_name: "txns".to_owned(),
485                    handle_purpose: "commit txns".to_owned(),
486                },
487            )
488            .await
489            .expect("txns schema shouldn't change");
490
491        let txns_read = TxnsRead::start::<TxnsCodecRow>(txns_client.clone(), txns_id).await;
492
493        let collections = Arc::new(std::sync::Mutex::new(BTreeMap::default()));
494        let finalizable_shards =
495            Arc::new(ShardIdSet::new(metrics.finalization_outstanding.clone()));
496        let finalized_shards =
497            Arc::new(ShardIdSet::new(metrics.finalization_pending_commit.clone()));
498        let config = Arc::new(Mutex::new(StorageConfiguration::new(
499            connection_context,
500            mz_dyncfgs::all_dyncfgs(),
501        )));
502
503        let initial_txn_upper = txns_write.fetch_recent_upper().await.to_owned();
504
505        let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
506        let (holds_tx, holds_rx) = mpsc::unbounded_channel();
507        let mut background_task = BackgroundTask {
508            config: Arc::clone(&config),
509            cmds_tx: cmd_tx.clone(),
510            cmds_rx: cmd_rx,
511            holds_rx,
512            collections: Arc::clone(&collections),
513            finalizable_shards: Arc::clone(&finalizable_shards),
514            shard_by_id: BTreeMap::new(),
515            since_handles: BTreeMap::new(),
516            txns_handle: Some(txns_write),
517            txns_shards: Default::default(),
518        };
519
520        let background_task =
521            mz_ore::task::spawn(|| "storage_collections::background_task", async move {
522                background_task.run().await
523            });
524
525        let finalize_shards_task = mz_ore::task::spawn(
526            || "storage_collections::finalize_shards_task",
527            finalize_shards_task(FinalizeShardsTaskConfig {
528                envd_epoch: envd_epoch.clone(),
529                config: Arc::clone(&config),
530                metrics,
531                finalizable_shards: Arc::clone(&finalizable_shards),
532                finalized_shards: Arc::clone(&finalized_shards),
533                persist_location: persist_location.clone(),
534                persist: Arc::clone(&persist_clients),
535                read_only,
536            }),
537        );
538
539        Self {
540            finalizable_shards,
541            finalized_shards,
542            collections,
543            txns_read,
544            envd_epoch,
545            read_only,
546            config,
547            initial_txn_upper,
548            persist_location,
549            persist: persist_clients,
550            cmd_tx,
551            holds_tx,
552            _background_task: Arc::new(background_task.abort_on_drop()),
553            _finalize_shards_task: Arc::new(finalize_shards_task.abort_on_drop()),
554        }
555    }
556
557    /// Opens a [WriteHandle] and a [SinceHandleWrapper], for holding back the since.
558    ///
559    /// `since` is an optional since that the read handle will be forwarded to
560    /// if it is less than its current since.
561    ///
562    /// This will `halt!` the process if we cannot successfully acquire a
563    /// critical handle with our current epoch.
564    async fn open_data_handles(
565        &self,
566        id: &GlobalId,
567        shard: ShardId,
568        since: Option<&Antichain<Timestamp>>,
569        relation_desc: RelationDesc,
570        persist_client: &PersistClient,
571    ) -> (
572        WriteHandle<SourceData, (), Timestamp, StorageDiff>,
573        SinceHandleWrapper,
574    ) {
575        let since_handle = if self.read_only {
576            let read_handle = self
577                .open_leased_handle(id, shard, relation_desc.clone(), since, persist_client)
578                .await;
579            SinceHandleWrapper::Leased(read_handle)
580        } else {
581            // We're managing the data for this shard in read-write mode, which would fence out other
582            // processes in read-only mode; it's safe to upgrade the metadata version.
583            persist_client
584                .upgrade_version::<SourceData, (), Timestamp, StorageDiff>(
585                    shard,
586                    Diagnostics {
587                        shard_name: id.to_string(),
588                        handle_purpose: format!("controller data for {}", id),
589                    },
590                )
591                .await
592                .expect("invalid persist usage");
593
594            let since_handle = self
595                .open_critical_handle(id, shard, since, persist_client)
596                .await;
597
598            SinceHandleWrapper::Critical(since_handle)
599        };
600
601        let mut write_handle = self
602            .open_write_handle(id, shard, relation_desc, persist_client)
603            .await;
604
605        // N.B.
606        // Fetch the most recent upper for the write handle. Otherwise, this may
607        // be behind the since of the since handle. Its vital this happens AFTER
608        // we create the since handle as it needs to be linearized with that
609        // operation. It may be true that creating the write handle after the
610        // since handle already ensures this, but we do this out of an abundance
611        // of caution.
612        //
613        // Note that this returns the upper, but also sets it on the handle to
614        // be fetched later.
615        write_handle.fetch_recent_upper().await;
616
617        (write_handle, since_handle)
618    }
619
620    /// Opens a write handle for the given `shard`.
621    async fn open_write_handle(
622        &self,
623        id: &GlobalId,
624        shard: ShardId,
625        relation_desc: RelationDesc,
626        persist_client: &PersistClient,
627    ) -> WriteHandle<SourceData, (), Timestamp, StorageDiff> {
628        let diagnostics = Diagnostics {
629            shard_name: id.to_string(),
630            handle_purpose: format!("controller data for {}", id),
631        };
632
633        let write = persist_client
634            .open_writer(
635                shard,
636                Arc::new(relation_desc),
637                Arc::new(UnitSchema),
638                diagnostics.clone(),
639            )
640            .await
641            .expect("invalid persist usage");
642
643        write
644    }
645
646    /// Opens a critical since handle for the given `shard`.
647    ///
648    /// `since` is an optional since that the read handle will be forwarded to
649    /// if it is less than its current since.
650    ///
651    /// This will `halt!` the process if we cannot successfully acquire a
652    /// critical handle with our current epoch.
653    async fn open_critical_handle(
654        &self,
655        id: &GlobalId,
656        shard: ShardId,
657        since: Option<&Antichain<Timestamp>>,
658        persist_client: &PersistClient,
659    ) -> SinceHandle<SourceData, (), Timestamp, StorageDiff> {
660        tracing::debug!(%id, ?since, "opening critical handle");
661
662        assert!(
663            !self.read_only,
664            "attempting to open critical SinceHandle in read-only mode"
665        );
666
667        let diagnostics = Diagnostics {
668            shard_name: id.to_string(),
669            handle_purpose: format!("controller data for {}", id),
670        };
671
672        // Construct the handle in a separate block to ensure all error paths
673        // are diverging
674        let since_handle = {
675            // This block's aim is to ensure the handle is in terms of our epoch
676            // by the time we return it.
677            let mut handle = persist_client
678                .open_critical_since(
679                    shard,
680                    PersistClient::CONTROLLER_CRITICAL_SINCE,
681                    Opaque::encode(&PersistEpoch::default()),
682                    diagnostics.clone(),
683                )
684                .await
685                .expect("invalid persist usage");
686
687            // Take the join of the handle's since and the provided `since`;
688            // this lets materialized views express the since at which their
689            // read handles "start."
690            let provided_since = match since {
691                Some(since) => since,
692                None => &Antichain::from_elem(Timestamp::MIN),
693            };
694            let since = handle.since().join(provided_since);
695
696            let our_epoch = self.envd_epoch;
697
698            loop {
699                let current_epoch: PersistEpoch = handle.opaque().decode();
700
701                // Ensure the current epoch is <= our epoch.
702                let unchecked_success = current_epoch.0.map(|e| e <= our_epoch).unwrap_or(true);
703
704                if unchecked_success {
705                    // Update the handle's state so that it is in terms of our
706                    // epoch.
707                    let checked_success = handle
708                        .compare_and_downgrade_since(
709                            &Opaque::encode(&current_epoch),
710                            (&Opaque::encode(&PersistEpoch::from(our_epoch)), &since),
711                        )
712                        .await
713                        .is_ok();
714                    if checked_success {
715                        break handle;
716                    }
717                } else {
718                    mz_ore::halt!("fenced by envd @ {current_epoch:?}. ours = {our_epoch}");
719                }
720            }
721        };
722
723        since_handle
724    }
725
726    /// Opens a leased [ReadHandle], for the purpose of holding back a since,
727    /// for the given `shard`.
728    ///
729    /// `since` is an optional since that the read handle will be forwarded to
730    /// if it is less than its current since.
731    async fn open_leased_handle(
732        &self,
733        id: &GlobalId,
734        shard: ShardId,
735        relation_desc: RelationDesc,
736        since: Option<&Antichain<Timestamp>>,
737        persist_client: &PersistClient,
738    ) -> ReadHandle<SourceData, (), Timestamp, StorageDiff> {
739        tracing::debug!(%id, ?since, "opening leased handle");
740
741        let diagnostics = Diagnostics {
742            shard_name: id.to_string(),
743            handle_purpose: format!("controller data for {}", id),
744        };
745
746        let use_critical_since = false;
747        let mut handle: ReadHandle<_, _, _, _> = persist_client
748            .open_leased_reader(
749                shard,
750                Arc::new(relation_desc),
751                Arc::new(UnitSchema),
752                diagnostics.clone(),
753                use_critical_since,
754            )
755            .await
756            .expect("invalid persist usage");
757
758        // Take the join of the handle's since and the provided `since`;
759        // this lets materialized views express the since at which their
760        // read handles "start."
761        let provided_since = match since {
762            Some(since) => since,
763            None => &Antichain::from_elem(Timestamp::MIN),
764        };
765        let since = handle.since().join(provided_since);
766
767        handle.downgrade_since(&since).await;
768
769        handle
770    }
771
772    fn register_handles(
773        &self,
774        id: GlobalId,
775        is_in_txns: bool,
776        since_handle: SinceHandleWrapper,
777        write_handle: WriteHandle<SourceData, (), Timestamp, StorageDiff>,
778    ) {
779        self.send(BackgroundCmd::Register {
780            id,
781            is_in_txns,
782            since_handle,
783            write_handle,
784        });
785    }
786
787    fn send(&self, cmd: BackgroundCmd) {
788        let _ = self.cmd_tx.send(cmd);
789    }
790
791    async fn snapshot_stats_inner(
792        &self,
793        id: GlobalId,
794        as_of: SnapshotStatsAsOf,
795    ) -> Result<SnapshotStats, StorageError> {
796        // TODO: Pull this out of BackgroundTask. Unlike the other methods, the
797        // caller of this one drives it to completion.
798        //
799        // We'd need to either share the critical handle somehow or maybe have
800        // two instances around, one in the worker and one in the
801        // StorageCollections.
802        let (tx, rx) = oneshot::channel();
803        self.send(BackgroundCmd::SnapshotStats(id, as_of, tx));
804        rx.await.expect("BackgroundTask should be live").0.await
805    }
806
807    /// If this identified collection has a dependency, install a read hold on
808    /// it.
809    ///
810    /// This is necessary to ensure that the dependency's since does not advance
811    /// beyond its dependents'.
812    fn install_collection_dependency_read_holds_inner(
813        &self,
814        self_collections: &mut BTreeMap<GlobalId, CollectionState>,
815        id: GlobalId,
816    ) -> Result<(), StorageError> {
817        let (deps, collection_implied_capability) = match self_collections.get(&id) {
818            Some(CollectionState {
819                storage_dependencies: deps,
820                implied_capability,
821                ..
822            }) => (deps.clone(), implied_capability),
823            _ => return Ok(()),
824        };
825
826        for dep in deps.iter() {
827            let dep_collection = self_collections
828                .get(dep)
829                .ok_or(StorageError::IdentifierMissing(id))?;
830
831            mz_ore::soft_assert_or_log!(
832                PartialOrder::less_equal(
833                    &dep_collection.implied_capability,
834                    collection_implied_capability
835                ),
836                "dependency since ({dep}@{:?}) cannot be in advance of dependent's since ({id}@{:?})",
837                dep_collection.implied_capability,
838                collection_implied_capability,
839            );
840        }
841
842        self.install_read_capabilities_inner(
843            self_collections,
844            id,
845            &deps,
846            collection_implied_capability.clone(),
847        )?;
848
849        Ok(())
850    }
851
852    /// Returns the given collection's dependencies.
853    fn determine_collection_dependencies(
854        self_collections: &BTreeMap<GlobalId, CollectionState>,
855        source_id: GlobalId,
856        collection_desc: &CollectionDescription,
857    ) -> Result<Vec<GlobalId>, StorageError> {
858        let mut dependencies = Vec::new();
859
860        if let Some(id) = collection_desc.primary {
861            dependencies.push(id);
862        }
863
864        match &collection_desc.data_source {
865            DataSource::Introspection(_)
866            | DataSource::Webhook
867            | DataSource::Table
868            | DataSource::Progress
869            | DataSource::Other => (),
870            DataSource::IngestionExport {
871                ingestion_id,
872                data_config,
873                ..
874            } => {
875                // Ingestion exports depend on their primary source's remap
876                // collection, except when they use a CDCv2 envelope.
877                let source = self_collections
878                    .get(ingestion_id)
879                    .ok_or(StorageError::IdentifierMissing(*ingestion_id))?;
880                let Some(remap_collection_id) = &source.ingestion_remap_collection_id else {
881                    panic!("SourceExport must refer to a primary source that already exists");
882                };
883
884                match data_config.envelope {
885                    SourceEnvelope::CdcV2 => (),
886                    _ => dependencies.push(*remap_collection_id),
887                }
888            }
889            // Ingestions depend on their remap collection.
890            DataSource::Ingestion(ingestion) => {
891                if ingestion.remap_collection_id != source_id {
892                    dependencies.push(ingestion.remap_collection_id);
893                }
894            }
895            DataSource::Sink { desc } => dependencies.push(desc.sink.from),
896        }
897
898        Ok(dependencies)
899    }
900
901    /// Install read capabilities on the given `storage_dependencies`.
902    #[instrument(level = "debug")]
903    fn install_read_capabilities_inner(
904        &self,
905        self_collections: &mut BTreeMap<GlobalId, CollectionState>,
906        from_id: GlobalId,
907        storage_dependencies: &[GlobalId],
908        read_capability: Antichain<Timestamp>,
909    ) -> Result<(), StorageError> {
910        let mut changes = ChangeBatch::new();
911        for time in read_capability.iter() {
912            changes.update(*time, 1);
913        }
914
915        if tracing::span_enabled!(tracing::Level::TRACE) {
916            // Collecting `user_capabilities` is potentially slow, thus only do it when needed.
917            let user_capabilities = self_collections
918                .iter_mut()
919                .filter(|(id, _c)| id.is_user())
920                .map(|(id, c)| {
921                    let updates = c.read_capabilities.updates().cloned().collect_vec();
922                    (*id, c.implied_capability.clone(), updates)
923                })
924                .collect_vec();
925
926            trace!(
927                %from_id,
928                ?storage_dependencies,
929                ?read_capability,
930                ?user_capabilities,
931                "install_read_capabilities_inner");
932        }
933
934        let mut storage_read_updates = storage_dependencies
935            .iter()
936            .map(|id| (*id, changes.clone()))
937            .collect();
938
939        StorageCollectionsImpl::update_read_capabilities_inner(
940            &self.cmd_tx,
941            self_collections,
942            &mut storage_read_updates,
943        );
944
945        if tracing::span_enabled!(tracing::Level::TRACE) {
946            // Collecting `user_capabilities` is potentially slow, thus only do it when needed.
947            let user_capabilities = self_collections
948                .iter_mut()
949                .filter(|(id, _c)| id.is_user())
950                .map(|(id, c)| {
951                    let updates = c.read_capabilities.updates().cloned().collect_vec();
952                    (*id, c.implied_capability.clone(), updates)
953                })
954                .collect_vec();
955
956            trace!(
957                %from_id,
958                ?storage_dependencies,
959                ?read_capability,
960                ?user_capabilities,
961                "after install_read_capabilities_inner!");
962        }
963
964        Ok(())
965    }
966
967    async fn recent_upper(&self, id: GlobalId) -> Result<Antichain<Timestamp>, StorageError> {
968        let metadata = &self.collection_metadata(id)?;
969        let persist_client = self
970            .persist
971            .open(metadata.persist_location.clone())
972            .await
973            .unwrap();
974        // Duplicate part of open_data_handles here because we don't need the
975        // fetch_recent_upper call. The pubsub-updated shared_upper is enough.
976        let diagnostics = Diagnostics {
977            shard_name: id.to_string(),
978            handle_purpose: format!("controller data for {}", id),
979        };
980        // NB: Opening a WriteHandle is cheap if it's never used in a
981        // compare_and_append operation.
982        let write = persist_client
983            .open_writer::<SourceData, (), Timestamp, StorageDiff>(
984                metadata.data_shard,
985                Arc::new(metadata.relation_desc.clone()),
986                Arc::new(UnitSchema),
987                diagnostics.clone(),
988            )
989            .await
990            .expect("invalid persist usage");
991        Ok(write.shared_upper())
992    }
993
994    async fn read_handle_for_snapshot(
995        persist: Arc<PersistClientCache>,
996        metadata: &CollectionMetadata,
997        id: GlobalId,
998    ) -> Result<ReadHandle<SourceData, (), Timestamp, StorageDiff>, StorageError> {
999        let persist_client = persist
1000            .open(metadata.persist_location.clone())
1001            .await
1002            .unwrap();
1003
1004        // We create a new read handle every time someone requests a snapshot
1005        // and then immediately expire it instead of keeping a read handle
1006        // permanently in our state to avoid having it heartbeat continually.
1007        // The assumption is that calls to snapshot are rare and therefore worth
1008        // it to always create a new handle.
1009        let read_handle = persist_client
1010            .open_leased_reader::<SourceData, (), _, _>(
1011                metadata.data_shard,
1012                Arc::new(metadata.relation_desc.clone()),
1013                Arc::new(UnitSchema),
1014                Diagnostics {
1015                    shard_name: id.to_string(),
1016                    handle_purpose: format!("snapshot {}", id),
1017                },
1018                USE_CRITICAL_SINCE_SNAPSHOT.get(&persist.cfg),
1019            )
1020            .await
1021            .expect("invalid persist usage");
1022        Ok(read_handle)
1023    }
1024
1025    fn snapshot(
1026        &self,
1027        id: GlobalId,
1028        as_of: Timestamp,
1029        txns_read: &TxnsRead<Timestamp>,
1030    ) -> BoxFuture<'static, Result<Vec<(Row, StorageDiff)>, StorageError>> {
1031        let metadata = match self.collection_metadata(id) {
1032            Ok(metadata) => metadata.clone(),
1033            Err(e) => return async { Err(e.into()) }.boxed(),
1034        };
1035        let txns_read = metadata.txns_shard.as_ref().map(|txns_id| {
1036            assert_eq!(txns_id, txns_read.txns_id());
1037            txns_read.clone()
1038        });
1039        let persist = Arc::clone(&self.persist);
1040        async move {
1041            let mut read_handle = Self::read_handle_for_snapshot(persist, &metadata, id).await?;
1042            let contents = match txns_read {
1043                None => {
1044                    // We're not using txn-wal for tables, so we can take a snapshot directly.
1045                    read_handle
1046                        .snapshot_and_fetch(Antichain::from_elem(as_of))
1047                        .await
1048                }
1049                Some(txns_read) => {
1050                    // We _are_ using txn-wal for tables. It advances the physical upper of the
1051                    // shard lazily, so we need to ask it for the snapshot to ensure the read is
1052                    // unblocked.
1053                    //
1054                    // Consider the following scenario:
1055                    // - Table A is written to via txns at time 5
1056                    // - Tables other than A are written to via txns consuming timestamps up to 10
1057                    // - We'd like to read A at 7
1058                    // - The application process of A's txn has advanced the upper to 5+1, but we need
1059                    //   it to be past 7, but the txns shard knows that (5,10) is empty of writes to A
1060                    // - This branch allows it to handle that advancing the physical upper of Table A to
1061                    //   10 (NB but only once we see it get past the write at 5!)
1062                    // - Then we can read it normally.
1063                    txns_read.update_gt(as_of).await;
1064                    let data_snapshot = txns_read.data_snapshot(metadata.data_shard, as_of).await;
1065                    data_snapshot.snapshot_and_fetch(&mut read_handle).await
1066                }
1067            };
1068            match contents {
1069                Ok(contents) => {
1070                    let mut snapshot = Vec::with_capacity(contents.len());
1071                    for ((data, _), _, diff) in contents {
1072                        // TODO(petrosagg): We should accumulate the errors too and let the user
1073                        // interpret the result
1074                        let row = data.0?;
1075                        snapshot.push((row, diff));
1076                    }
1077                    Ok(snapshot)
1078                }
1079                Err(_) => Err(StorageError::ReadBeforeSince(id)),
1080            }
1081        }
1082        .boxed()
1083    }
1084
1085    fn snapshot_and_stream(
1086        &self,
1087        id: GlobalId,
1088        as_of: Timestamp,
1089        txns_read: &TxnsRead<Timestamp>,
1090    ) -> BoxFuture<'static, Result<SourceDataStream, StorageError>> {
1091        use futures::stream::StreamExt;
1092
1093        let metadata = match self.collection_metadata(id) {
1094            Ok(metadata) => metadata.clone(),
1095            Err(e) => return async { Err(e.into()) }.boxed(),
1096        };
1097        let txns_read = metadata.txns_shard.as_ref().map(|txns_id| {
1098            assert_eq!(txns_id, txns_read.txns_id());
1099            txns_read.clone()
1100        });
1101        let persist = Arc::clone(&self.persist);
1102
1103        async move {
1104            let mut read_handle = Self::read_handle_for_snapshot(persist, &metadata, id).await?;
1105            let stream = match txns_read {
1106                None => {
1107                    // We're not using txn-wal for tables, so we can take a snapshot directly.
1108                    read_handle
1109                        .snapshot_and_stream(Antichain::from_elem(as_of))
1110                        .await
1111                        .map_err(|_| StorageError::ReadBeforeSince(id))?
1112                        .boxed()
1113                }
1114                Some(txns_read) => {
1115                    txns_read.update_gt(as_of).await;
1116                    let data_snapshot = txns_read.data_snapshot(metadata.data_shard, as_of).await;
1117                    data_snapshot
1118                        .snapshot_and_stream(&mut read_handle)
1119                        .await
1120                        .map_err(|_| StorageError::ReadBeforeSince(id))?
1121                        .boxed()
1122                }
1123            };
1124
1125            // Map our stream, unwrapping Persist internal errors.
1126            let stream = stream.map(|((data, _v), t, d)| (data, t, d)).boxed();
1127            Ok(stream)
1128        }
1129        .boxed()
1130    }
1131
1132    fn set_read_policies_inner(
1133        &self,
1134        collections: &mut BTreeMap<GlobalId, CollectionState>,
1135        policies: Vec<(GlobalId, ReadPolicy)>,
1136    ) {
1137        trace!("set_read_policies: {:?}", policies);
1138
1139        let mut read_capability_changes = BTreeMap::default();
1140
1141        for (id, policy) in policies.into_iter() {
1142            let collection = match collections.get_mut(&id) {
1143                Some(c) => c,
1144                None => {
1145                    panic!("Reference to absent collection {id}");
1146                }
1147            };
1148
1149            let mut new_read_capability = policy.frontier(collection.write_frontier.borrow());
1150
1151            if PartialOrder::less_equal(&collection.implied_capability, &new_read_capability) {
1152                let mut update = ChangeBatch::new();
1153                update.extend(new_read_capability.iter().map(|time| (*time, 1)));
1154                std::mem::swap(&mut collection.implied_capability, &mut new_read_capability);
1155                update.extend(new_read_capability.iter().map(|time| (*time, -1)));
1156                if !update.is_empty() {
1157                    read_capability_changes.insert(id, update);
1158                }
1159            }
1160
1161            collection.read_policy = policy;
1162        }
1163
1164        for (id, changes) in read_capability_changes.iter() {
1165            if id.is_user() {
1166                trace!(%id, ?changes, "in set_read_policies, capability changes");
1167            }
1168        }
1169
1170        if !read_capability_changes.is_empty() {
1171            StorageCollectionsImpl::update_read_capabilities_inner(
1172                &self.cmd_tx,
1173                collections,
1174                &mut read_capability_changes,
1175            );
1176        }
1177    }
1178
1179    // This is not an associated function so that we can share it with the task
1180    // that updates the persist handles and also has a reference to the shared
1181    // collections state.
1182    fn update_read_capabilities_inner(
1183        cmd_tx: &mpsc::UnboundedSender<BackgroundCmd>,
1184        collections: &mut BTreeMap<GlobalId, CollectionState>,
1185        updates: &mut BTreeMap<GlobalId, ChangeBatch<Timestamp>>,
1186    ) {
1187        // Location to record consequences that we need to act on.
1188        let mut collections_net = BTreeMap::new();
1189
1190        // We must not rely on any specific relative ordering of `GlobalId`s.
1191        // That said, it is reasonable to assume that collections generally have
1192        // greater IDs than their dependencies, so starting with the largest is
1193        // a useful optimization.
1194        while let Some(id) = updates.keys().rev().next().cloned() {
1195            let mut update = updates.remove(&id).unwrap();
1196
1197            if id.is_user() {
1198                trace!(id = ?id, update = ?update, "update_read_capabilities");
1199            }
1200
1201            let collection = if let Some(c) = collections.get_mut(&id) {
1202                c
1203            } else {
1204                let has_positive_updates = update.iter().any(|(_ts, diff)| *diff > 0);
1205                if has_positive_updates {
1206                    panic!(
1207                        "reference to absent collection {id} but we have positive updates: {:?}",
1208                        update
1209                    );
1210                } else {
1211                    // Continue purely negative updates. Someone has probably
1212                    // already dropped this collection!
1213                    continue;
1214                }
1215            };
1216
1217            let current_read_capabilities = collection.read_capabilities.frontier().to_owned();
1218            for (time, diff) in update.iter() {
1219                assert!(
1220                    collection.read_capabilities.count_for(time) + diff >= 0,
1221                    "update {:?} for collection {id} would lead to negative \
1222                        read capabilities, read capabilities before applying: {:?}",
1223                    update,
1224                    collection.read_capabilities
1225                );
1226
1227                if collection.read_capabilities.count_for(time) + diff > 0 {
1228                    assert!(
1229                        current_read_capabilities.less_equal(time),
1230                        "update {:?} for collection {id} is trying to \
1231                            install read capabilities before the current \
1232                            frontier of read capabilities, read capabilities before applying: {:?}",
1233                        update,
1234                        collection.read_capabilities
1235                    );
1236                }
1237            }
1238
1239            let changes = collection.read_capabilities.update_iter(update.drain());
1240            update.extend(changes);
1241
1242            if id.is_user() {
1243                trace!(
1244                %id,
1245                ?collection.storage_dependencies,
1246                ?update,
1247                "forwarding update to storage dependencies");
1248            }
1249
1250            for id in collection.storage_dependencies.iter() {
1251                updates
1252                    .entry(*id)
1253                    .or_insert_with(ChangeBatch::new)
1254                    .extend(update.iter().cloned());
1255            }
1256
1257            let (changes, frontier) = collections_net
1258                .entry(id)
1259                .or_insert_with(|| (<ChangeBatch<_>>::new(), Antichain::new()));
1260
1261            changes.extend(update.drain());
1262            *frontier = collection.read_capabilities.frontier().to_owned();
1263        }
1264
1265        // Translate our net compute actions into downgrades of persist sinces.
1266        // The actual downgrades are performed by a Tokio task asynchronously.
1267        let mut persist_compaction_commands = Vec::with_capacity(collections_net.len());
1268        for (key, (mut changes, frontier)) in collections_net {
1269            if !changes.is_empty() {
1270                // If the collection has a "primary" collection, let that primary drive compaction.
1271                let collection = collections.get(&key).expect("must still exist");
1272                let should_emit_persist_compaction = collection.primary.is_none();
1273
1274                if frontier.is_empty() {
1275                    info!(id = %key, "removing collection state because the since advanced to []!");
1276                    collections.remove(&key).expect("must still exist");
1277                }
1278
1279                if should_emit_persist_compaction {
1280                    persist_compaction_commands.push((key, frontier));
1281                }
1282            }
1283        }
1284
1285        if !persist_compaction_commands.is_empty() {
1286            cmd_tx
1287                .send(BackgroundCmd::DowngradeSince(persist_compaction_commands))
1288                .expect("cannot fail to send");
1289        }
1290    }
1291
1292    /// Remove any shards that we know are finalized
1293    fn synchronize_finalized_shards(&self, storage_metadata: &StorageMetadata) {
1294        self.finalized_shards
1295            .lock()
1296            .retain(|shard| storage_metadata.unfinalized_shards.contains(shard));
1297    }
1298}
1299
1300/// Partitions the finalization WAL by whether an active collection references
1301/// each shard.
1302fn partition_finalizable_shards(
1303    collection_metadata: BTreeMap<GlobalId, ShardId>,
1304    active_collection_ids: &BTreeSet<GlobalId>,
1305    unfinalized_shards: BTreeSet<ShardId>,
1306) -> (BTreeSet<ShardId>, BTreeSet<ShardId>) {
1307    let active_shards: BTreeSet<_> = collection_metadata
1308        .into_iter()
1309        .filter_map(|(id, shard)| active_collection_ids.contains(&id).then_some(shard))
1310        .collect();
1311    let referenced_shards = unfinalized_shards
1312        .intersection(&active_shards)
1313        .copied()
1314        .collect();
1315    let finalizable_shards = unfinalized_shards
1316        .difference(&active_shards)
1317        .copied()
1318        .collect();
1319
1320    (referenced_shards, finalizable_shards)
1321}
1322
1323// See comments on the above impl for StorageCollectionsImpl.
1324#[async_trait]
1325impl StorageCollections for StorageCollectionsImpl {
1326    async fn initialize_state(
1327        &self,
1328        txn: &mut (dyn StorageTxn + Send),
1329        init_ids: BTreeSet<GlobalId>,
1330    ) -> Result<(), StorageError> {
1331        let metadata = txn.get_collection_metadata();
1332        let existing_metadata: BTreeSet<_> = metadata.into_iter().map(|(id, _)| id).collect();
1333
1334        // Determine which collections we do not yet have metadata for.
1335        let new_collections: BTreeSet<GlobalId> =
1336            init_ids.difference(&existing_metadata).cloned().collect();
1337
1338        self.prepare_state(
1339            txn,
1340            new_collections,
1341            BTreeSet::default(),
1342            BTreeMap::default(),
1343        )
1344        .await?;
1345
1346        // All unreferenced shards that belong to collections dropped in the
1347        // last epoch are eligible for finalization. Active collection metadata
1348        // is authoritative over stale finalization WAL entries.
1349        //
1350        // A dropped collection can still have a dataflow running on a worker.
1351        // Finalizing its shard on reboot can cause that worker to panic.
1352        let (referenced_shards, unfinalized_shards) = partition_finalizable_shards(
1353            txn.get_collection_metadata(),
1354            &init_ids,
1355            txn.get_unfinalized_shards(),
1356        );
1357        if !referenced_shards.is_empty() {
1358            warn!(
1359                ?referenced_shards,
1360                "removing active collection shards from the finalization WAL"
1361            );
1362            // Collection metadata is authoritative. Removing these entries in
1363            // the initialization transaction keeps them out of the finalizer
1364            // queue.
1365            txn.remove_unfinalized_shards(referenced_shards);
1366        }
1367
1368        info!(?unfinalized_shards, "initializing finalizable_shards");
1369
1370        self.finalizable_shards.lock().extend(unfinalized_shards);
1371
1372        Ok(())
1373    }
1374
1375    fn update_parameters(&self, config_params: StorageParameters) {
1376        // We serialize the dyncfg updates in StorageParameters, but configure
1377        // persist separately.
1378        config_params.dyncfg_updates.apply(self.persist.cfg());
1379
1380        self.config
1381            .lock()
1382            .expect("lock poisoned")
1383            .update(config_params);
1384    }
1385
1386    fn collection_metadata(&self, id: GlobalId) -> Result<CollectionMetadata, CollectionMissing> {
1387        let collections = self.collections.lock().expect("lock poisoned");
1388
1389        collections
1390            .get(&id)
1391            .map(|c| c.collection_metadata.clone())
1392            .ok_or(CollectionMissing(id))
1393    }
1394
1395    fn active_collection_metadatas(&self) -> Vec<(GlobalId, CollectionMetadata)> {
1396        let collections = self.collections.lock().expect("lock poisoned");
1397
1398        collections
1399            .iter()
1400            .filter(|(_id, c)| !c.is_dropped())
1401            .map(|(id, c)| (*id, c.collection_metadata.clone()))
1402            .collect()
1403    }
1404
1405    fn collections_frontiers(
1406        &self,
1407        ids: Vec<GlobalId>,
1408    ) -> Result<Vec<CollectionFrontiers>, CollectionMissing> {
1409        if ids.is_empty() {
1410            return Ok(vec![]);
1411        }
1412
1413        let collections = self.collections.lock().expect("lock poisoned");
1414
1415        let res = ids
1416            .into_iter()
1417            .map(|id| {
1418                collections
1419                    .get(&id)
1420                    .map(|c| CollectionFrontiers {
1421                        id: id.clone(),
1422                        write_frontier: c.write_frontier.clone(),
1423                        implied_capability: c.implied_capability.clone(),
1424                        read_capabilities: c.read_capabilities.frontier().to_owned(),
1425                    })
1426                    .ok_or(CollectionMissing(id))
1427            })
1428            .collect::<Result<Vec<_>, _>>()?;
1429
1430        Ok(res)
1431    }
1432
1433    fn active_collection_frontiers(&self) -> Vec<CollectionFrontiers> {
1434        let collections = self.collections.lock().expect("lock poisoned");
1435
1436        let res = collections
1437            .iter()
1438            .filter(|(_id, c)| !c.is_dropped())
1439            .map(|(id, c)| CollectionFrontiers {
1440                id: id.clone(),
1441                write_frontier: c.write_frontier.clone(),
1442                implied_capability: c.implied_capability.clone(),
1443                read_capabilities: c.read_capabilities.frontier().to_owned(),
1444            })
1445            .collect_vec();
1446
1447        res
1448    }
1449
1450    async fn snapshot_stats(
1451        &self,
1452        id: GlobalId,
1453        as_of: Antichain<Timestamp>,
1454    ) -> Result<SnapshotStats, StorageError> {
1455        let metadata = self.collection_metadata(id)?;
1456
1457        // See the comments in StorageController::snapshot for what's going on
1458        // here.
1459        let as_of = match metadata.txns_shard.as_ref() {
1460            None => SnapshotStatsAsOf::Direct(as_of),
1461            Some(txns_id) => {
1462                assert_eq!(txns_id, self.txns_read.txns_id());
1463                let as_of = as_of
1464                    .into_option()
1465                    .expect("cannot read as_of the empty antichain");
1466                self.txns_read.update_gt(as_of).await;
1467                let data_snapshot = self
1468                    .txns_read
1469                    .data_snapshot(metadata.data_shard, as_of)
1470                    .await;
1471                SnapshotStatsAsOf::Txns(data_snapshot)
1472            }
1473        };
1474        self.snapshot_stats_inner(id, as_of).await
1475    }
1476
1477    async fn snapshot_parts_stats(
1478        &self,
1479        id: GlobalId,
1480        as_of: Antichain<Timestamp>,
1481    ) -> BoxFuture<'static, Result<SnapshotPartsStats, StorageError>> {
1482        let metadata = {
1483            let self_collections = self.collections.lock().expect("lock poisoned");
1484
1485            let collection_metadata = self_collections
1486                .get(&id)
1487                .ok_or(StorageError::IdentifierMissing(id))
1488                .map(|c| c.collection_metadata.clone());
1489
1490            match collection_metadata {
1491                Ok(m) => m,
1492                Err(e) => return Box::pin(async move { Err(e) }),
1493            }
1494        };
1495
1496        // See the comments in StorageController::snapshot for what's going on
1497        // here.
1498        let persist = Arc::clone(&self.persist);
1499        let read_handle = Self::read_handle_for_snapshot(persist, &metadata, id).await;
1500
1501        let data_snapshot = match (metadata, as_of.as_option()) {
1502            (
1503                CollectionMetadata {
1504                    txns_shard: Some(txns_id),
1505                    data_shard,
1506                    ..
1507                },
1508                Some(as_of),
1509            ) => {
1510                assert_eq!(txns_id, *self.txns_read.txns_id());
1511                self.txns_read.update_gt(*as_of).await;
1512                let data_snapshot = self.txns_read.data_snapshot(data_shard, *as_of).await;
1513                Some(data_snapshot)
1514            }
1515            _ => None,
1516        };
1517
1518        Box::pin(async move {
1519            let read_handle = read_handle?;
1520            let result = match data_snapshot {
1521                Some(data_snapshot) => data_snapshot.snapshot_parts_stats(&read_handle).await,
1522                None => read_handle.snapshot_parts_stats(as_of).await,
1523            };
1524            read_handle.expire().await;
1525            result.map_err(|_| StorageError::ReadBeforeSince(id))
1526        })
1527    }
1528
1529    fn snapshot(
1530        &self,
1531        id: GlobalId,
1532        as_of: Timestamp,
1533    ) -> BoxFuture<'static, Result<Vec<(Row, StorageDiff)>, StorageError>> {
1534        self.snapshot(id, as_of, &self.txns_read)
1535    }
1536
1537    async fn snapshot_latest(&self, id: GlobalId) -> Result<Vec<Row>, StorageError> {
1538        let upper = self.recent_upper(id).await?;
1539        let res = match upper.as_option() {
1540            Some(f) if f > &Timestamp::MIN => {
1541                let as_of = f.step_back().expect("checked that f > &Timestamp::MIN");
1542
1543                let snapshot = self.snapshot(id, as_of, &self.txns_read).await?;
1544                snapshot
1545                    .into_iter()
1546                    .map(|(row, diff)| {
1547                        // See the trait doc: `snapshot_latest` is only meant for collections that
1548                        // consolidate to a set.
1549                        assert_eq!(diff, 1, "snapshot doesn't accumulate to set");
1550                        row
1551                    })
1552                    .collect()
1553            }
1554            Some(_min) => {
1555                // The collection must be empty!
1556                Vec::new()
1557            }
1558            // The collection is closed, we cannot determine a latest read
1559            // timestamp based on the upper.
1560            _ => {
1561                return Err(StorageError::InvalidUsage(
1562                    "collection closed, cannot determine a read timestamp based on the upper"
1563                        .to_string(),
1564                ));
1565            }
1566        };
1567
1568        Ok(res)
1569    }
1570
1571    fn snapshot_cursor(
1572        &self,
1573        id: GlobalId,
1574        as_of: Timestamp,
1575    ) -> BoxFuture<'static, Result<SnapshotCursor, StorageError>> {
1576        let metadata = match self.collection_metadata(id) {
1577            Ok(metadata) => metadata.clone(),
1578            Err(e) => return async { Err(e.into()) }.boxed(),
1579        };
1580        let txns_read = metadata.txns_shard.as_ref().map(|txns_id| {
1581            // Ensure the txn's shard the controller has is the same that this
1582            // collection is registered to.
1583            assert_eq!(txns_id, self.txns_read.txns_id());
1584            self.txns_read.clone()
1585        });
1586        let persist = Arc::clone(&self.persist);
1587
1588        // See the comments in Self::snapshot for what's going on here.
1589        async move {
1590            let mut handle = Self::read_handle_for_snapshot(persist, &metadata, id).await?;
1591            let cursor = match txns_read {
1592                None => {
1593                    let cursor = handle
1594                        .snapshot_cursor(Antichain::from_elem(as_of), |_| true)
1595                        .await
1596                        .map_err(|_| StorageError::ReadBeforeSince(id))?;
1597                    SnapshotCursor {
1598                        _read_handle: handle,
1599                        cursor,
1600                    }
1601                }
1602                Some(txns_read) => {
1603                    txns_read.update_gt(as_of).await;
1604                    let data_snapshot = txns_read.data_snapshot(metadata.data_shard, as_of).await;
1605                    let cursor = data_snapshot
1606                        .snapshot_cursor(&mut handle, |_| true)
1607                        .await
1608                        .map_err(|_| StorageError::ReadBeforeSince(id))?;
1609                    SnapshotCursor {
1610                        _read_handle: handle,
1611                        cursor,
1612                    }
1613                }
1614            };
1615
1616            Ok(cursor)
1617        }
1618        .boxed()
1619    }
1620
1621    fn snapshot_and_stream(
1622        &self,
1623        id: GlobalId,
1624        as_of: Timestamp,
1625    ) -> BoxFuture<
1626        'static,
1627        Result<BoxStream<'static, (SourceData, Timestamp, StorageDiff)>, StorageError>,
1628    > {
1629        self.snapshot_and_stream(id, as_of, &self.txns_read)
1630    }
1631
1632    fn create_update_builder(
1633        &self,
1634        id: GlobalId,
1635    ) -> BoxFuture<
1636        'static,
1637        Result<TimestamplessUpdateBuilder<SourceData, (), StorageDiff>, StorageError>,
1638    > {
1639        let metadata = match self.collection_metadata(id) {
1640            Ok(m) => m,
1641            Err(e) => return Box::pin(async move { Err(e.into()) }),
1642        };
1643        let persist = Arc::clone(&self.persist);
1644
1645        async move {
1646            let persist_client = persist
1647                .open(metadata.persist_location.clone())
1648                .await
1649                .expect("invalid persist usage");
1650            let write_handle = persist_client
1651                .open_writer::<SourceData, (), Timestamp, StorageDiff>(
1652                    metadata.data_shard,
1653                    Arc::new(metadata.relation_desc.clone()),
1654                    Arc::new(UnitSchema),
1655                    Diagnostics {
1656                        shard_name: id.to_string(),
1657                        handle_purpose: format!("create write batch {}", id),
1658                    },
1659                )
1660                .await
1661                .expect("invalid persist usage");
1662            let builder = TimestamplessUpdateBuilder::new(&write_handle);
1663
1664            Ok(builder)
1665        }
1666        .boxed()
1667    }
1668
1669    fn check_exists(&self, id: GlobalId) -> Result<(), StorageError> {
1670        let collections = self.collections.lock().expect("lock poisoned");
1671
1672        if collections.contains_key(&id) {
1673            Ok(())
1674        } else {
1675            Err(StorageError::IdentifierMissing(id))
1676        }
1677    }
1678
1679    async fn prepare_state(
1680        &self,
1681        txn: &mut (dyn StorageTxn + Send),
1682        ids_to_add: BTreeSet<GlobalId>,
1683        ids_to_drop: BTreeSet<GlobalId>,
1684        ids_to_register: BTreeMap<GlobalId, ShardId>,
1685    ) -> Result<(), StorageError> {
1686        // Durable metadata can outlive its collection. Reconcile it with live
1687        // collection state so orphaned mappings do not block finalization.
1688        let mut active_collection_ids: BTreeSet<_> = {
1689            let collections = self.collections.lock().expect("poisoned");
1690            collections
1691                .iter()
1692                .filter_map(|(id, collection)| {
1693                    (!ids_to_drop.contains(id) && !collection.is_dropped()).then_some(*id)
1694                })
1695                .collect()
1696        };
1697        active_collection_ids.extend(ids_to_add.iter().copied());
1698        active_collection_ids.extend(ids_to_register.keys().copied());
1699
1700        txn.insert_collection_metadata(
1701            ids_to_add
1702                .into_iter()
1703                .map(|id| (id, ShardId::new()))
1704                .collect(),
1705        )?;
1706        txn.insert_collection_metadata(ids_to_register)?;
1707
1708        // Delete the metadata for any dropped collections.
1709        let dropped_mappings = txn.delete_collection_metadata(ids_to_drop);
1710
1711        // Only finalize the shards of dropped collections that don't have a primary.
1712        // Otherwise the shard might still be in use by the primary.
1713        let mut dropped_shards = BTreeSet::new();
1714        {
1715            let collections = self.collections.lock().expect("poisoned");
1716            for (id, shard) in dropped_mappings {
1717                let coll = collections.get(&id).expect("must exist");
1718                if coll.primary.is_none() {
1719                    dropped_shards.insert(shard);
1720                }
1721            }
1722        }
1723        let remaining_metadata = txn.get_collection_metadata();
1724        let (referenced_shards, dropped_shards) = partition_finalizable_shards(
1725            remaining_metadata,
1726            &active_collection_ids,
1727            dropped_shards,
1728        );
1729        if !referenced_shards.is_empty() {
1730            mz_ore::soft_panic_or_log!(
1731                "dropped collections would finalize shards that active collections still use: \
1732                 {referenced_shards:?}"
1733            );
1734        }
1735        txn.insert_unfinalized_shards(dropped_shards)?;
1736
1737        // Reconcile any shards we've successfully finalized with the shard
1738        // finalization collection.
1739        let finalized_shards = self.finalized_shards.lock().iter().copied().collect();
1740        txn.remove_unfinalized_shards(finalized_shards);
1741
1742        Ok(())
1743    }
1744
1745    // TODO(aljoscha): It would be swell if we could refactor this Leviathan of
1746    // a method/move individual parts to their own methods.
1747    #[instrument(level = "debug")]
1748    async fn create_collections_for_bootstrap(
1749        &self,
1750        storage_metadata: &StorageMetadata,
1751        register_ts: Option<Timestamp>,
1752        mut collections: Vec<(GlobalId, CollectionDescription)>,
1753        migrated_storage_collections: &BTreeSet<GlobalId>,
1754    ) -> Result<(), StorageError> {
1755        let is_in_txns = |id, metadata: &CollectionMetadata| {
1756            metadata.txns_shard.is_some()
1757                && !(self.read_only && migrated_storage_collections.contains(&id))
1758        };
1759
1760        // Validate first, to avoid corrupting state.
1761        // 1. create a dropped identifier, or
1762        // 2. create an existing identifier with a new description.
1763        // Make sure to check for errors within `ingestions` as well.
1764        collections.sort_by_key(|(id, _)| *id);
1765        collections.dedup();
1766        for pos in 1..collections.len() {
1767            if collections[pos - 1].0 == collections[pos].0 {
1768                return Err(StorageError::CollectionIdReused(collections[pos].0));
1769            }
1770        }
1771
1772        // We first enrich each collection description with some additional
1773        // metadata...
1774        let enriched_with_metadata = collections
1775            .into_iter()
1776            .map(|(id, description)| {
1777                let data_shard = storage_metadata.get_collection_shard(id)?;
1778
1779                // If the shard is being managed by txn-wal (initially,
1780                // tables), then we need to pass along the shard id for the txns
1781                // shard to dataflow rendering.
1782                let txns_shard = description
1783                    .data_source
1784                    .in_txns()
1785                    .then(|| *self.txns_read.txns_id());
1786
1787                let metadata = CollectionMetadata {
1788                    persist_location: self.persist_location.clone(),
1789                    data_shard,
1790                    relation_desc: description.desc.clone(),
1791                    txns_shard,
1792                };
1793
1794                Ok((id, description, metadata))
1795            })
1796            .collect_vec();
1797
1798        // So that we can open `SinceHandle`s for each collections concurrently.
1799        let persist_client = self
1800            .persist
1801            .open(self.persist_location.clone())
1802            .await
1803            .unwrap();
1804        let persist_client = &persist_client;
1805        // Reborrow the `&mut self` as immutable, as all the concurrent work to
1806        // be processed in this stream cannot all have exclusive access.
1807        use futures::stream::{StreamExt, TryStreamExt};
1808        let this = &*self;
1809        let mut to_register: Vec<_> = futures::stream::iter(enriched_with_metadata)
1810            .map(|data: Result<_, StorageError>| {
1811                async move {
1812                    let (id, description, metadata) = data?;
1813
1814                    // should be replaced with real introspection
1815                    // (https://github.com/MaterializeInc/database-issues/issues/4078)
1816                    // but for now, it's helpful to have this mapping written down
1817                    // somewhere
1818                    debug!("mapping GlobalId={} to shard ({})", id, metadata.data_shard);
1819
1820                    // If this collection has a primary, the primary is responsible for downgrading
1821                    // the critical since and it would be an error if we did so here while opening
1822                    // the since handle.
1823                    let since = if description.primary.is_some() {
1824                        None
1825                    } else {
1826                        description.since.as_ref()
1827                    };
1828
1829                    let (write, mut since_handle) = this
1830                        .open_data_handles(
1831                            &id,
1832                            metadata.data_shard,
1833                            since,
1834                            metadata.relation_desc.clone(),
1835                            persist_client,
1836                        )
1837                        .await;
1838
1839                    // Present tables as springing into existence at the register_ts
1840                    // by advancing the since. Otherwise, we could end up in a
1841                    // situation where a table with a long compaction window appears
1842                    // to exist before the environment (and this the table) existed.
1843                    //
1844                    // We could potentially also do the same thing for other
1845                    // sources, in particular storage's internal sources and perhaps
1846                    // others, but leave them for now.
1847                    match description.data_source {
1848                        DataSource::Introspection(_)
1849                        | DataSource::IngestionExport { .. }
1850                        | DataSource::Webhook
1851                        | DataSource::Ingestion(_)
1852                        | DataSource::Progress
1853                        | DataSource::Other => {}
1854                        DataSource::Sink { .. } => {}
1855                        DataSource::Table => {
1856                            let register_ts = register_ts.expect(
1857                                "caller should have provided a register_ts when creating a table",
1858                            );
1859                            if since_handle.since().elements() == &[Timestamp::MIN]
1860                                && !migrated_storage_collections.contains(&id)
1861                            {
1862                                debug!("advancing {} to initial since of {:?}", id, register_ts);
1863                                let token = since_handle.opaque();
1864                                let _ = since_handle
1865                                    .compare_and_downgrade_since(
1866                                        &token,
1867                                        (&token, &Antichain::from_elem(register_ts)),
1868                                    )
1869                                    .await;
1870                            }
1871                        }
1872                    }
1873
1874                    Ok::<_, StorageError>((id, description, write, since_handle, metadata))
1875                }
1876            })
1877            // Poll each future for each collection concurrently, maximum of 50 at a time.
1878            .buffer_unordered(50)
1879            // HERE BE DRAGONS:
1880            //
1881            // There are at least 2 subtleties in using `FuturesUnordered`
1882            // (which `buffer_unordered` uses underneath:
1883            // - One is captured here
1884            //   <https://github.com/rust-lang/futures-rs/issues/2387>
1885            // - And the other is deadlocking if processing an OUTPUT of a
1886            //   `FuturesUnordered` stream attempts to obtain an async mutex that
1887            //   is also obtained in the futures being polled.
1888            //
1889            // Both of these could potentially be issues in all usages of
1890            // `buffer_unordered` in this method, so we stick the standard
1891            // advice: only use `try_collect` or `collect`!
1892            .try_collect()
1893            .await?;
1894
1895        // Reorder in dependency order.
1896        #[derive(Ord, PartialOrd, Eq, PartialEq)]
1897        enum DependencyOrder {
1898            /// Tables should always be registered first, and large ids before small ones.
1899            Table(Reverse<GlobalId>),
1900            /// For most collections the id order is the correct one.
1901            Collection(GlobalId),
1902            /// Sinks should always be registered last.
1903            Sink(GlobalId),
1904        }
1905        to_register.sort_by_key(|(id, desc, ..)| match &desc.data_source {
1906            DataSource::Table => DependencyOrder::Table(Reverse(*id)),
1907            DataSource::Sink { .. } => DependencyOrder::Sink(*id),
1908            _ => DependencyOrder::Collection(*id),
1909        });
1910
1911        // We hold this lock for a very short amount of time, just doing some
1912        // hashmap inserts and unbounded channel sends.
1913        let mut self_collections = self.collections.lock().expect("lock poisoned");
1914
1915        for (id, description, write_handle, since_handle, metadata) in to_register {
1916            let write_frontier = write_handle.upper();
1917            let data_shard_since = since_handle.since().clone();
1918
1919            // Determine if this collection has any dependencies.
1920            let storage_dependencies =
1921                Self::determine_collection_dependencies(&*self_collections, id, &description)?;
1922
1923            // Determine the initial since of the collection.
1924            let initial_since = match storage_dependencies
1925                .iter()
1926                .at_most_one()
1927                .expect("should have at most one dependency")
1928            {
1929                Some(dep) => {
1930                    let dependency_collection = self_collections
1931                        .get(dep)
1932                        .ok_or(StorageError::IdentifierMissing(*dep))?;
1933                    let dependency_since = dependency_collection.implied_capability.clone();
1934
1935                    // If an item has a dependency, its initial since must be
1936                    // advanced as far as its dependency, i.e. a dependency's
1937                    // since may never be in advance of its dependents.
1938                    //
1939                    // We have to do this every time we initialize the
1940                    // collection, though––the invariant might have been upheld
1941                    // correctly in the previous epoch, but the
1942                    // `data_shard_since` might not have compacted and, on
1943                    // establishing a new persist connection, still have data we
1944                    // said _could_ be compacted.
1945                    if PartialOrder::less_than(&data_shard_since, &dependency_since) {
1946                        // The dependency since cannot be beyond the dependent
1947                        // (our) upper unless the collection is new. In
1948                        // practice, the depdenency is the remap shard of a
1949                        // source (export), and if the since is allowed to
1950                        // "catch up" to the upper, that is `upper <= since`, a
1951                        // restarting ingestion cannot differentiate between
1952                        // updates that have already been written out to the
1953                        // backing persist shard and updates that have yet to be
1954                        // written. We would write duplicate updates.
1955                        //
1956                        // If this check fails, it means that the read hold
1957                        // installed on the dependency was probably not upheld
1958                        // –– if it were, the dependency's since could not have
1959                        // advanced as far the dependent's upper.
1960                        //
1961                        // We don't care about the dependency since when the
1962                        // write frontier is empty. In that case, no-one can
1963                        // write down any more updates.
1964                        // This invariant applies to remap dependencies. A `primary`
1965                        // dependency is another version of the same shard, whose since can
1966                        // validly equal the dependent's upper.
1967                        if description.primary.is_none() {
1968                            mz_ore::soft_assert_or_log!(
1969                                write_frontier.elements() == &[Timestamp::MIN]
1970                                    || write_frontier.is_empty()
1971                                    || PartialOrder::less_than(&dependency_since, write_frontier),
1972                                "dependency ({dep}) since has advanced past dependent ({id}) upper \n
1973                                dependent ({id}): since {:?}, upper {:?} \n
1974                                dependency ({dep}): since {:?}",
1975                                data_shard_since,
1976                                write_frontier,
1977                                dependency_since
1978                            );
1979                        }
1980
1981                        dependency_since
1982                    } else {
1983                        data_shard_since
1984                    }
1985                }
1986                None => data_shard_since,
1987            };
1988
1989            // Determine the time dependence of the collection.
1990            let time_dependence = {
1991                use DataSource::*;
1992                if let Some(timeline) = &description.timeline
1993                    && *timeline != Timeline::EpochMilliseconds
1994                {
1995                    // Only the epoch timeline follows wall-clock.
1996                    None
1997                } else {
1998                    match &description.data_source {
1999                        Ingestion(ingestion) => {
2000                            use GenericSourceConnection::*;
2001                            match ingestion.desc.connection {
2002                                // Kafka, Postgres, MySql, and SQL Server sources all
2003                                // follow wall clock.
2004                                Kafka(_) | Postgres(_) | MySql(_) | SqlServer(_) => {
2005                                    Some(TimeDependence::default())
2006                                }
2007                                // Load generators not further specified.
2008                                LoadGenerator(_) => None,
2009                            }
2010                        }
2011                        IngestionExport { ingestion_id, .. } => {
2012                            let c = self_collections.get(ingestion_id).expect("known to exist");
2013                            c.time_dependence.clone()
2014                        }
2015                        // Introspection, other, progress, table, and webhook sources follow wall clock.
2016                        Introspection(_) | Progress | Table { .. } | Webhook { .. } => {
2017                            Some(TimeDependence::default())
2018                        }
2019                        // Materialized views, etc, aren't managed by storage.
2020                        Other => None,
2021                        Sink { .. } => None,
2022                    }
2023                }
2024            };
2025
2026            let ingestion_remap_collection_id = match &description.data_source {
2027                DataSource::Ingestion(desc) => Some(desc.remap_collection_id),
2028                _ => None,
2029            };
2030
2031            let mut collection_state = CollectionState::new(
2032                description.primary,
2033                time_dependence,
2034                ingestion_remap_collection_id,
2035                initial_since,
2036                write_frontier.clone(),
2037                storage_dependencies,
2038                metadata.clone(),
2039            );
2040
2041            // Install the collection state in the appropriate spot.
2042            match &description.data_source {
2043                DataSource::Introspection(_) => {
2044                    self_collections.insert(id, collection_state);
2045                }
2046                DataSource::Webhook => {
2047                    self_collections.insert(id, collection_state);
2048                }
2049                DataSource::IngestionExport { .. } => {
2050                    self_collections.insert(id, collection_state);
2051                }
2052                DataSource::Table => {
2053                    // See comment on self.initial_txn_upper on why we're doing
2054                    // this.
2055                    if is_in_txns(id, &metadata)
2056                        && PartialOrder::less_than(
2057                            &collection_state.write_frontier,
2058                            &self.initial_txn_upper,
2059                        )
2060                    {
2061                        // We could try and be cute and use the join of the txn
2062                        // upper and the table upper. But that has more
2063                        // complicated reasoning for why it is or isn't correct,
2064                        // and we're only dealing with totally ordered times
2065                        // here.
2066                        collection_state
2067                            .write_frontier
2068                            .clone_from(&self.initial_txn_upper);
2069                    }
2070                    self_collections.insert(id, collection_state);
2071                }
2072                DataSource::Progress | DataSource::Other => {
2073                    self_collections.insert(id, collection_state);
2074                }
2075                DataSource::Ingestion(_) => {
2076                    self_collections.insert(id, collection_state);
2077                }
2078                DataSource::Sink { .. } => {
2079                    self_collections.insert(id, collection_state);
2080                }
2081            }
2082
2083            self.register_handles(id, is_in_txns(id, &metadata), since_handle, write_handle);
2084
2085            // If this collection has a dependency, install a read hold on it.
2086            self.install_collection_dependency_read_holds_inner(&mut *self_collections, id)?;
2087        }
2088
2089        drop(self_collections);
2090
2091        self.synchronize_finalized_shards(storage_metadata);
2092
2093        Ok(())
2094    }
2095
2096    async fn alter_table_desc(
2097        &self,
2098        existing_collection: GlobalId,
2099        new_collection: GlobalId,
2100        new_desc: RelationDesc,
2101        expected_version: RelationVersion,
2102    ) -> Result<(), StorageError> {
2103        let data_shard = {
2104            let self_collections = self.collections.lock().expect("lock poisoned");
2105            let existing = self_collections
2106                .get(&existing_collection)
2107                .ok_or_else(|| StorageError::IdentifierMissing(existing_collection))?;
2108
2109            existing.collection_metadata.data_shard
2110        };
2111
2112        let persist_client = self
2113            .persist
2114            .open(self.persist_location.clone())
2115            .await
2116            .unwrap();
2117
2118        // Evolve the schema of this shard.
2119        let diagnostics = Diagnostics {
2120            shard_name: existing_collection.to_string(),
2121            handle_purpose: "alter_table_desc".to_string(),
2122        };
2123        // We map the Adapter's RelationVersion 1:1 with SchemaId.
2124        let expected_schema = expected_version.into();
2125        let schema_result = persist_client
2126            .compare_and_evolve_schema::<SourceData, (), Timestamp, StorageDiff>(
2127                data_shard,
2128                expected_schema,
2129                &new_desc,
2130                &UnitSchema,
2131                diagnostics,
2132            )
2133            .await
2134            .map_err(|e| StorageError::InvalidUsage(e.to_string()))?;
2135        tracing::info!(
2136            ?existing_collection,
2137            ?new_collection,
2138            ?new_desc,
2139            "evolved schema"
2140        );
2141
2142        match schema_result {
2143            // id unused: the desc is re-derived below.
2144            CaESchema::Ok(_id) => {}
2145            // Not retried by design. compare_and_evolve_schema now absorbs a
2146            // committed-but-lost CaS, so an ExpectedMismatch here is a genuine
2147            // divergence (current matches neither the request nor `expected`)
2148            // that must surface rather than retry.
2149            CaESchema::ExpectedMismatch {
2150                schema_id,
2151                key,
2152                val,
2153            } => {
2154                mz_ore::soft_panic_or_log!(
2155                    "schema expectation mismatch, expected {expected_schema:?} \
2156                     but found {schema_id:?}, {key:?}, {val:?}"
2157                );
2158                return Err(StorageError::Generic(anyhow::anyhow!(
2159                    "schema expected mismatch, {existing_collection:?}",
2160                )));
2161            }
2162            CaESchema::Incompatible => {
2163                mz_ore::soft_panic_or_log!(
2164                    "incompatible schema! {existing_collection} {new_desc:?}"
2165                );
2166                return Err(StorageError::Generic(anyhow::anyhow!(
2167                    "schema incompatible, {existing_collection:?}"
2168                )));
2169            }
2170        };
2171
2172        // Once the new schema is registered we can open new data handles.
2173        let (write_handle, since_handle) = self
2174            .open_data_handles(
2175                &new_collection,
2176                data_shard,
2177                None,
2178                new_desc.clone(),
2179                &persist_client,
2180            )
2181            .await;
2182
2183        // TODO(alter_table): Do we need to advance the since of the table to match the time this
2184        // new version was registered with txn-wal?
2185
2186        // Great! Our new schema is registered with Persist, now we need to update our internal
2187        // data structures.
2188        {
2189            let mut self_collections = self.collections.lock().expect("lock poisoned");
2190
2191            // Update the existing collection so we know it's a "projection" of this new one.
2192            let existing = self_collections
2193                .get_mut(&existing_collection)
2194                .expect("existing collection missing");
2195
2196            // A higher level should already be asserting this, but let's make sure.
2197            assert_none!(existing.primary);
2198
2199            // The existing version of the table will depend on the new version.
2200            existing.primary = Some(new_collection);
2201            existing.storage_dependencies.push(new_collection);
2202
2203            // Copy over the frontiers from the previous version.
2204            // The new table starts with two holds - the implied capability, and the hold from
2205            // the previous version - both at the previous version's read frontier.
2206            let implied_capability = existing.read_capabilities.frontier().to_owned();
2207            let write_frontier = existing.write_frontier.clone();
2208
2209            // Determine the relevant read capabilities on the new collection.
2210            //
2211            // Note(parkmycar): Originally we used `install_collection_dependency_read_holds_inner`
2212            // here, but that only installed a ReadHold on the new collection for the implied
2213            // capability of the existing collection. This would cause runtime panics because it
2214            // would eventually result in negative read capabilities.
2215            let mut changes = ChangeBatch::new();
2216            changes.extend(implied_capability.iter().map(|t| (*t, 1)));
2217
2218            // Note: The new collection is now the "primary collection".
2219            let collection_meta = CollectionMetadata {
2220                persist_location: self.persist_location.clone(),
2221                relation_desc: new_desc.clone(),
2222                data_shard,
2223                txns_shard: Some(self.txns_read.txns_id().clone()),
2224            };
2225            let collection_state = CollectionState::new(
2226                None,
2227                existing.time_dependence.clone(),
2228                existing.ingestion_remap_collection_id.clone(),
2229                implied_capability,
2230                write_frontier,
2231                Vec::new(),
2232                collection_meta,
2233            );
2234
2235            // Add a record of the new collection.
2236            self_collections.insert(new_collection, collection_state);
2237
2238            let mut updates = BTreeMap::from([(new_collection, changes)]);
2239            StorageCollectionsImpl::update_read_capabilities_inner(
2240                &self.cmd_tx,
2241                &mut *self_collections,
2242                &mut updates,
2243            );
2244        };
2245
2246        // TODO(alter_table): Support changes to sources.
2247        self.register_handles(new_collection, true, since_handle, write_handle);
2248
2249        info!(%existing_collection, %new_collection, ?new_desc, "altered table");
2250
2251        Ok(())
2252    }
2253
2254    fn drop_collections_unvalidated(
2255        &self,
2256        storage_metadata: &StorageMetadata,
2257        identifiers: Vec<GlobalId>,
2258    ) {
2259        debug!(?identifiers, "drop_collections_unvalidated");
2260
2261        let mut self_collections = self.collections.lock().expect("lock poisoned");
2262        // Durable metadata can outlive its collection. Reconcile it with live
2263        // collection state so orphaned mappings do not block finalization.
2264        let dropping: BTreeSet<_> = identifiers.iter().copied().collect();
2265        let active_collection_ids: BTreeSet<_> = self_collections
2266            .iter()
2267            .filter_map(|(id, collection)| {
2268                (!dropping.contains(id) && !collection.is_dropped()).then_some(*id)
2269            })
2270            .collect();
2271        let shards_in_use: BTreeSet<_> = storage_metadata
2272            .collection_metadata
2273            .iter()
2274            .filter_map(|(id, shard)| active_collection_ids.contains(id).then_some(*shard))
2275            .collect();
2276
2277        // Policies that advance the since to the empty antichain. We do still
2278        // honor outstanding read holds, and collections will only be dropped
2279        // once those are removed as well.
2280        //
2281        // We don't explicitly remove read capabilities! Downgrading the
2282        // frontier of the source to `[]` (the empty Antichain), will propagate
2283        // to the storage dependencies.
2284        let mut finalized_policies = Vec::new();
2285
2286        for id in identifiers {
2287            // Make sure it's still there, might already have been deleted.
2288            let Some(collection) = self_collections.get(&id) else {
2289                continue;
2290            };
2291
2292            // Unless the collection has a primary, its shard must have been previously removed
2293            // by `StorageCollections::prepare_state`.
2294            if collection.primary.is_none() {
2295                let metadata = storage_metadata.get_collection_shard(id);
2296                mz_ore::soft_assert_or_log!(
2297                    matches!(metadata, Err(StorageError::IdentifierMissing(_))),
2298                    "dropping {id}, but drop was not synchronized with storage \
2299                     controller via `prepare_state`"
2300                );
2301
2302                // Releasing the owner's since can destroy a shared shard even if the
2303                // finalization WAL is guarded. Prefer leaking this collection state over
2304                // destroying data when durable metadata contradicts the primary links.
2305                let data_shard = collection.collection_metadata.data_shard;
2306                if shards_in_use.contains(&data_shard) {
2307                    mz_ore::soft_panic_or_log!(
2308                        "dropping {id} would release the since of shard {data_shard}, \
2309                         which an active collection still uses"
2310                    );
2311                    continue;
2312                }
2313            }
2314
2315            finalized_policies.push((id, ReadPolicy::ValidFrom(Antichain::new())));
2316        }
2317
2318        self.set_read_policies_inner(&mut self_collections, finalized_policies);
2319
2320        drop(self_collections);
2321
2322        self.synchronize_finalized_shards(storage_metadata);
2323    }
2324
2325    fn set_read_policies(&self, policies: Vec<(GlobalId, ReadPolicy)>) {
2326        let mut collections = self.collections.lock().expect("lock poisoned");
2327
2328        if tracing::enabled!(tracing::Level::TRACE) {
2329            let user_capabilities = collections
2330                .iter_mut()
2331                .filter(|(id, _c)| id.is_user())
2332                .map(|(id, c)| {
2333                    let updates = c.read_capabilities.updates().cloned().collect_vec();
2334                    (*id, c.implied_capability.clone(), updates)
2335                })
2336                .collect_vec();
2337
2338            trace!(?policies, ?user_capabilities, "set_read_policies");
2339        }
2340
2341        self.set_read_policies_inner(&mut collections, policies);
2342
2343        if tracing::enabled!(tracing::Level::TRACE) {
2344            let user_capabilities = collections
2345                .iter_mut()
2346                .filter(|(id, _c)| id.is_user())
2347                .map(|(id, c)| {
2348                    let updates = c.read_capabilities.updates().cloned().collect_vec();
2349                    (*id, c.implied_capability.clone(), updates)
2350                })
2351                .collect_vec();
2352
2353            trace!(?user_capabilities, "after! set_read_policies");
2354        }
2355    }
2356
2357    fn acquire_read_holds(
2358        &self,
2359        desired_holds: Vec<GlobalId>,
2360    ) -> Result<Vec<ReadHold>, CollectionMissing> {
2361        if desired_holds.is_empty() {
2362            return Ok(vec![]);
2363        }
2364
2365        let mut collections = self.collections.lock().expect("lock poisoned");
2366
2367        let mut advanced_holds = Vec::new();
2368        // We advance the holds by our current since frontier. Can't acquire
2369        // holds for times that have been compacted away!
2370        //
2371        // NOTE: We acquire read holds at the earliest possible time rather than
2372        // at the implied capability. This is so that, for example, adapter can
2373        // acquire a read hold to hold back the frontier, giving the COMPUTE
2374        // controller a chance to also acquire a read hold at that early
2375        // frontier. If/when we change the interplay between adapter and COMPUTE
2376        // to pass around ReadHold tokens, we might tighten this up and instead
2377        // acquire read holds at the implied capability.
2378        for id in desired_holds.iter() {
2379            let collection = collections.get(id).ok_or(CollectionMissing(*id))?;
2380            let since = collection.read_capabilities.frontier().to_owned();
2381            advanced_holds.push((*id, since));
2382        }
2383
2384        let mut updates = advanced_holds
2385            .iter()
2386            .map(|(id, hold)| {
2387                let mut changes = ChangeBatch::new();
2388                changes.extend(hold.iter().map(|time| (*time, 1)));
2389                (*id, changes)
2390            })
2391            .collect::<BTreeMap<_, _>>();
2392
2393        StorageCollectionsImpl::update_read_capabilities_inner(
2394            &self.cmd_tx,
2395            &mut collections,
2396            &mut updates,
2397        );
2398
2399        let acquired_holds = advanced_holds
2400            .into_iter()
2401            .map(|(id, since)| ReadHold::with_channel(id, since, self.holds_tx.clone()))
2402            .collect_vec();
2403
2404        trace!(?desired_holds, ?acquired_holds, "acquire_read_holds");
2405
2406        Ok(acquired_holds)
2407    }
2408
2409    /// Determine time dependence information for the object.
2410    fn determine_time_dependence(
2411        &self,
2412        id: GlobalId,
2413    ) -> Result<Option<TimeDependence>, TimeDependenceError> {
2414        use TimeDependenceError::CollectionMissing;
2415        let collections = self.collections.lock().expect("lock poisoned");
2416        let state = collections.get(&id).ok_or(CollectionMissing(id))?;
2417        Ok(state.time_dependence.clone())
2418    }
2419
2420    fn dump(&self) -> Result<serde_json::Value, anyhow::Error> {
2421        // Destructure `self` here so we don't forget to consider dumping newly added fields.
2422        let Self {
2423            envd_epoch,
2424            read_only,
2425            finalizable_shards,
2426            finalized_shards,
2427            collections,
2428            txns_read: _,
2429            config,
2430            initial_txn_upper,
2431            persist_location,
2432            persist: _,
2433            cmd_tx: _,
2434            holds_tx: _,
2435            _background_task: _,
2436            _finalize_shards_task: _,
2437        } = self;
2438
2439        let finalizable_shards: Vec<_> = finalizable_shards
2440            .lock()
2441            .iter()
2442            .map(ToString::to_string)
2443            .collect();
2444        let finalized_shards: Vec<_> = finalized_shards
2445            .lock()
2446            .iter()
2447            .map(ToString::to_string)
2448            .collect();
2449        let collections: BTreeMap<_, _> = collections
2450            .lock()
2451            .expect("poisoned")
2452            .iter()
2453            .map(|(id, c)| (id.to_string(), format!("{c:?}")))
2454            .collect();
2455        let config = format!("{:?}", config.lock().expect("poisoned"));
2456
2457        Ok(serde_json::json!({
2458            "envd_epoch": envd_epoch,
2459            "read_only": read_only,
2460            "finalizable_shards": finalizable_shards,
2461            "finalized_shards": finalized_shards,
2462            "collections": collections,
2463            "config": config,
2464            "initial_txn_upper": initial_txn_upper,
2465            "persist_location": format!("{persist_location:?}"),
2466        }))
2467    }
2468}
2469
2470/// Wraps either a "critical" [SinceHandle] or a leased [ReadHandle].
2471///
2472/// When a [StorageCollections] is in read-only mode, we will only ever acquire
2473/// [ReadHandle], because acquiring the [SinceHandle] and driving forward its
2474/// since is considered a write. Conversely, when in read-write mode, we acquire
2475/// [SinceHandle].
2476#[derive(Debug)]
2477enum SinceHandleWrapper {
2478    Critical(SinceHandle<SourceData, (), Timestamp, StorageDiff>),
2479    Leased(ReadHandle<SourceData, (), Timestamp, StorageDiff>),
2480}
2481
2482impl SinceHandleWrapper {
2483    pub fn since(&self) -> &Antichain<Timestamp> {
2484        match self {
2485            Self::Critical(handle) => handle.since(),
2486            Self::Leased(handle) => handle.since(),
2487        }
2488    }
2489
2490    pub fn opaque(&self) -> PersistEpoch {
2491        match self {
2492            Self::Critical(handle) => handle.opaque().decode(),
2493            Self::Leased(_handle) => {
2494                // The opaque is expected to be used with
2495                // `compare_and_downgrade_since`, and the leased handle doesn't
2496                // have a notion of an opaque. We pretend here and in
2497                // `compare_and_downgrade_since`.
2498                PersistEpoch(None)
2499            }
2500        }
2501    }
2502
2503    pub async fn compare_and_downgrade_since(
2504        &mut self,
2505        expected: &PersistEpoch,
2506        (opaque, since): (&PersistEpoch, &Antichain<Timestamp>),
2507    ) -> Result<Antichain<Timestamp>, PersistEpoch> {
2508        match self {
2509            Self::Critical(handle) => handle
2510                .compare_and_downgrade_since(
2511                    &Opaque::encode(expected),
2512                    (&Opaque::encode(opaque), since),
2513                )
2514                .await
2515                .map_err(|e| e.decode()),
2516            Self::Leased(handle) => {
2517                assert_none!(opaque.0);
2518
2519                handle.downgrade_since(since).await;
2520
2521                Ok(since.clone())
2522            }
2523        }
2524    }
2525
2526    pub async fn maybe_compare_and_downgrade_since(
2527        &mut self,
2528        expected: &PersistEpoch,
2529        (opaque, since): (&PersistEpoch, &Antichain<Timestamp>),
2530    ) -> Option<Result<Antichain<Timestamp>, PersistEpoch>> {
2531        match self {
2532            Self::Critical(handle) => handle
2533                .maybe_compare_and_downgrade_since(
2534                    &Opaque::encode(expected),
2535                    (&Opaque::encode(opaque), since),
2536                )
2537                .await
2538                .map(|r| r.map_err(|o| o.decode())),
2539            Self::Leased(handle) => {
2540                assert_none!(opaque.0);
2541
2542                handle.maybe_downgrade_since(since).await;
2543
2544                Some(Ok(since.clone()))
2545            }
2546        }
2547    }
2548
2549    pub fn snapshot_stats(
2550        &self,
2551        id: GlobalId,
2552        as_of: Option<Antichain<Timestamp>>,
2553    ) -> BoxFuture<'static, Result<SnapshotStats, StorageError>> {
2554        match self {
2555            Self::Critical(handle) => {
2556                let res = handle
2557                    .snapshot_stats(as_of)
2558                    .map(move |x| x.map_err(|_| StorageError::ReadBeforeSince(id)));
2559                Box::pin(res)
2560            }
2561            Self::Leased(handle) => {
2562                let res = handle
2563                    .snapshot_stats(as_of)
2564                    .map(move |x| x.map_err(|_| StorageError::ReadBeforeSince(id)));
2565                Box::pin(res)
2566            }
2567        }
2568    }
2569
2570    pub fn snapshot_stats_from_txn(
2571        &self,
2572        id: GlobalId,
2573        data_snapshot: DataSnapshot<Timestamp>,
2574    ) -> BoxFuture<'static, Result<SnapshotStats, StorageError>> {
2575        match self {
2576            Self::Critical(handle) => Box::pin(
2577                data_snapshot
2578                    .snapshot_stats_from_critical(handle)
2579                    .map(move |x| x.map_err(|_| StorageError::ReadBeforeSince(id))),
2580            ),
2581            Self::Leased(handle) => Box::pin(
2582                data_snapshot
2583                    .snapshot_stats_from_leased(handle)
2584                    .map(move |x| x.map_err(|_| StorageError::ReadBeforeSince(id))),
2585            ),
2586        }
2587    }
2588}
2589
2590/// State maintained about individual collections.
2591#[derive(Debug, Clone)]
2592struct CollectionState {
2593    /// The primary of this collections.
2594    ///
2595    /// Multiple storage collections can point to the same persist shard,
2596    /// possibly with different schemas. In such a configuration, we select one
2597    /// of the involved collections as the primary, who "owns" the persist
2598    /// shard. All other involved collections have a dependency on the primary.
2599    primary: Option<GlobalId>,
2600
2601    /// Description of how this collection's frontier follows time.
2602    time_dependence: Option<TimeDependence>,
2603    /// The ID of the source remap/progress collection, if this is an ingestion.
2604    ingestion_remap_collection_id: Option<GlobalId>,
2605
2606    /// Accumulation of read capabilities for the collection.
2607    ///
2608    /// This accumulation will always contain `self.implied_capability`, but may
2609    /// also contain capabilities held by others who have read dependencies on
2610    /// this collection.
2611    pub read_capabilities: MutableAntichain<Timestamp>,
2612
2613    /// The implicit capability associated with collection creation.  This
2614    /// should never be less than the since of the associated persist
2615    /// collection.
2616    pub implied_capability: Antichain<Timestamp>,
2617
2618    /// The policy to use to downgrade `self.implied_capability`.
2619    pub read_policy: ReadPolicy,
2620
2621    /// Storage identifiers on which this collection depends.
2622    pub storage_dependencies: Vec<GlobalId>,
2623
2624    /// Reported write frontier.
2625    pub write_frontier: Antichain<Timestamp>,
2626
2627    pub collection_metadata: CollectionMetadata,
2628}
2629
2630impl CollectionState {
2631    /// Creates a new collection state, with an initial read policy valid from
2632    /// `since`.
2633    pub fn new(
2634        primary: Option<GlobalId>,
2635        time_dependence: Option<TimeDependence>,
2636        ingestion_remap_collection_id: Option<GlobalId>,
2637        since: Antichain<Timestamp>,
2638        write_frontier: Antichain<Timestamp>,
2639        storage_dependencies: Vec<GlobalId>,
2640        metadata: CollectionMetadata,
2641    ) -> Self {
2642        let mut read_capabilities = MutableAntichain::new();
2643        read_capabilities.update_iter(since.iter().map(|time| (*time, 1)));
2644        Self {
2645            primary,
2646            time_dependence,
2647            ingestion_remap_collection_id,
2648            read_capabilities,
2649            implied_capability: since.clone(),
2650            read_policy: ReadPolicy::NoPolicy {
2651                initial_since: since,
2652            },
2653            storage_dependencies,
2654            write_frontier,
2655            collection_metadata: metadata,
2656        }
2657    }
2658
2659    /// Returns whether the collection was dropped.
2660    pub fn is_dropped(&self) -> bool {
2661        self.read_capabilities.is_empty()
2662    }
2663}
2664
2665/// A task that keeps persist handles, downgrades sinces when asked,
2666/// periodically gets recent uppers from them, and updates the shard collection
2667/// state when needed.
2668///
2669/// This shares state with [StorageCollectionsImpl] via `Arcs` and channels.
2670#[derive(Debug)]
2671struct BackgroundTask {
2672    config: Arc<Mutex<StorageConfiguration>>,
2673    cmds_tx: mpsc::UnboundedSender<BackgroundCmd>,
2674    cmds_rx: mpsc::UnboundedReceiver<BackgroundCmd>,
2675    holds_rx: mpsc::UnboundedReceiver<(GlobalId, ChangeBatch<Timestamp>)>,
2676    finalizable_shards: Arc<ShardIdSet>,
2677    collections: Arc<std::sync::Mutex<BTreeMap<GlobalId, CollectionState>>>,
2678    // So we know what shard ID corresponds to what global ID, which we need
2679    // when re-enqueing futures for determining the next upper update.
2680    shard_by_id: BTreeMap<GlobalId, ShardId>,
2681    since_handles: BTreeMap<GlobalId, SinceHandleWrapper>,
2682    txns_handle: Option<WriteHandle<SourceData, (), Timestamp, StorageDiff>>,
2683    txns_shards: BTreeSet<GlobalId>,
2684}
2685
2686#[derive(Debug)]
2687enum BackgroundCmd {
2688    Register {
2689        id: GlobalId,
2690        is_in_txns: bool,
2691        write_handle: WriteHandle<SourceData, (), Timestamp, StorageDiff>,
2692        since_handle: SinceHandleWrapper,
2693    },
2694    DowngradeSince(Vec<(GlobalId, Antichain<Timestamp>)>),
2695    SnapshotStats(
2696        GlobalId,
2697        SnapshotStatsAsOf,
2698        oneshot::Sender<SnapshotStatsRes>,
2699    ),
2700}
2701
2702/// A newtype wrapper to hang a Debug impl off of.
2703pub(crate) struct SnapshotStatsRes(BoxFuture<'static, Result<SnapshotStats, StorageError>>);
2704
2705impl Debug for SnapshotStatsRes {
2706    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2707        f.debug_struct("SnapshotStatsRes").finish_non_exhaustive()
2708    }
2709}
2710
2711impl BackgroundTask {
2712    async fn run(&mut self) {
2713        // Futures that fetch the recent upper from all other shards.
2714        let mut upper_futures: FuturesUnordered<
2715            std::pin::Pin<
2716                Box<
2717                    dyn Future<
2718                            Output = (
2719                                GlobalId,
2720                                WriteHandle<SourceData, (), Timestamp, StorageDiff>,
2721                                Antichain<Timestamp>,
2722                            ),
2723                        > + Send,
2724                >,
2725            >,
2726        > = FuturesUnordered::new();
2727
2728        let gen_upper_future =
2729            |id, mut handle: WriteHandle<_, _, _, _>, prev_upper: Antichain<Timestamp>| {
2730                let fut = async move {
2731                    soft_assert_or_log!(
2732                        !prev_upper.is_empty(),
2733                        "cannot await progress when upper is already empty"
2734                    );
2735                    handle.wait_for_upper_past(&prev_upper).await;
2736                    let new_upper = handle.shared_upper();
2737                    (id, handle, new_upper)
2738                };
2739
2740                fut
2741            };
2742
2743        let mut txns_upper_future = match self.txns_handle.take() {
2744            Some(txns_handle) => {
2745                let upper = txns_handle.upper().clone();
2746                let txns_upper_future =
2747                    gen_upper_future(GlobalId::Transient(1), txns_handle, upper);
2748                txns_upper_future.boxed()
2749            }
2750            None => async { std::future::pending().await }.boxed(),
2751        };
2752
2753        loop {
2754            tokio::select! {
2755                (id, handle, upper) = &mut txns_upper_future => {
2756                    trace!("new upper from txns shard: {:?}", upper);
2757                    let mut uppers = Vec::new();
2758                    for id in self.txns_shards.iter() {
2759                        uppers.push((*id, &upper));
2760                    }
2761                    self.update_write_frontiers(&uppers).await;
2762
2763                    let fut = gen_upper_future(id, handle, upper);
2764                    txns_upper_future = fut.boxed();
2765                }
2766                Some((id, handle, upper)) = upper_futures.next() => {
2767                    if id.is_user() {
2768                        trace!("new upper for collection {id}: {:?}", upper);
2769                    }
2770                    let current_shard = self.shard_by_id.get(&id);
2771                    if let Some(shard_id) = current_shard {
2772                        if shard_id == &handle.shard_id() {
2773                            // Still current, so process the update and enqueue
2774                            // again!
2775                            let uppers = &[(id, &upper)];
2776                            self.update_write_frontiers(uppers).await;
2777                            if !upper.is_empty() {
2778                                let fut = gen_upper_future(id, handle, upper);
2779                                upper_futures.push(fut.boxed());
2780                            }
2781                        } else {
2782                            // Be polite and expire the write handle. This can
2783                            // happen when we get an upper update for a write
2784                            // handle that has since been replaced via Update.
2785                            handle.expire().await;
2786                        }
2787                    }
2788                }
2789                cmd = self.cmds_rx.recv() => {
2790                    let Some(cmd) = cmd else {
2791                        // We're done!
2792                        break;
2793                    };
2794
2795                    // Drain all commands so we can merge `DowngradeSince` requests. Without this
2796                    // optimization, downgrading sinces could fall behind in the face of a large
2797                    // amount of storage collections.
2798                    let commands = iter::once(cmd).chain(
2799                        iter::from_fn(|| self.cmds_rx.try_recv().ok())
2800                    );
2801                    let mut downgrades = BTreeMap::<_, Antichain<_>>::new();
2802                    for cmd in commands {
2803                        match cmd {
2804                            BackgroundCmd::Register{
2805                                id,
2806                                is_in_txns,
2807                                write_handle,
2808                                since_handle
2809                            } => {
2810                                debug!("registering handles for {}", id);
2811                                let previous = self.shard_by_id.insert(id, write_handle.shard_id());
2812                                if previous.is_some() {
2813                                    panic!("already registered a WriteHandle for collection {id}");
2814                                }
2815
2816                                let previous = self.since_handles.insert(id, since_handle);
2817                                if previous.is_some() {
2818                                    panic!("already registered a SinceHandle for collection {id}");
2819                                }
2820
2821                                if is_in_txns {
2822                                    self.txns_shards.insert(id);
2823                                } else {
2824                                    let upper = write_handle.upper().clone();
2825                                    if !upper.is_empty() {
2826                                        let fut = gen_upper_future(id, write_handle, upper);
2827                                        upper_futures.push(fut.boxed());
2828                                    }
2829                                }
2830                            }
2831                            BackgroundCmd::DowngradeSince(cmds) => {
2832                                for (id, new) in cmds {
2833                                    downgrades.entry(id)
2834                                        .and_modify(|since| since.join_assign(&new))
2835                                        .or_insert(new);
2836                                }
2837                            }
2838                            BackgroundCmd::SnapshotStats(id, as_of, tx) => {
2839                                // NB: The requested as_of could be arbitrarily far
2840                                // in the future. So, in order to avoid blocking
2841                                // this loop until it's available and the
2842                                // `snapshot_stats` call resolves, instead return
2843                                // the future to the caller and await it there.
2844                                let res = match self.since_handles.get(&id) {
2845                                    Some(x) => {
2846                                        let fut: BoxFuture<
2847                                            'static,
2848                                            Result<SnapshotStats, StorageError>,
2849                                        > = match as_of {
2850                                            SnapshotStatsAsOf::Direct(as_of) => {
2851                                                x.snapshot_stats(id, Some(as_of))
2852                                            }
2853                                            SnapshotStatsAsOf::Txns(data_snapshot) => {
2854                                                x.snapshot_stats_from_txn(id, data_snapshot)
2855                                            }
2856                                        };
2857                                        SnapshotStatsRes(fut)
2858                                    }
2859                                    None => SnapshotStatsRes(Box::pin(futures::future::ready(Err(
2860                                        StorageError::IdentifierMissing(id),
2861                                    )))),
2862                                };
2863                                // It's fine if the listener hung up.
2864                                let _ = tx.send(res);
2865                            }
2866                        }
2867                    }
2868
2869                    if !downgrades.is_empty() {
2870                        self.downgrade_sinces(downgrades).await;
2871                    }
2872                }
2873                Some(holds_changes) = self.holds_rx.recv() => {
2874                    let mut batched_changes = BTreeMap::new();
2875                    batched_changes.insert(holds_changes.0, holds_changes.1);
2876
2877                    while let Ok(mut holds_changes) = self.holds_rx.try_recv() {
2878                        let entry = batched_changes.entry(holds_changes.0);
2879                        entry
2880                            .and_modify(|existing| existing.extend(holds_changes.1.drain()))
2881                            .or_insert_with(|| holds_changes.1);
2882                    }
2883
2884                    let mut collections = self.collections.lock().expect("lock poisoned");
2885
2886                    let user_changes = batched_changes
2887                        .iter()
2888                        .filter(|(id, _c)| id.is_user())
2889                        .map(|(id, c)| {
2890                            (id.clone(), c.clone())
2891                        })
2892                        .collect_vec();
2893
2894                    if !user_changes.is_empty() {
2895                        trace!(?user_changes, "applying holds changes from channel");
2896                    }
2897
2898                    StorageCollectionsImpl::update_read_capabilities_inner(
2899                        &self.cmds_tx,
2900                        &mut collections,
2901                        &mut batched_changes,
2902                    );
2903                }
2904            }
2905        }
2906
2907        warn!("BackgroundTask shutting down");
2908    }
2909
2910    #[instrument(level = "debug")]
2911    async fn update_write_frontiers(&self, updates: &[(GlobalId, &Antichain<Timestamp>)]) {
2912        let mut read_capability_changes = BTreeMap::default();
2913
2914        let mut self_collections = self.collections.lock().expect("lock poisoned");
2915
2916        for (id, new_upper) in updates.iter() {
2917            let collection = if let Some(c) = self_collections.get_mut(id) {
2918                c
2919            } else {
2920                trace!(
2921                    "Reference to absent collection {id}, due to concurrent removal of that collection"
2922                );
2923                continue;
2924            };
2925
2926            if PartialOrder::less_than(&collection.write_frontier, *new_upper) {
2927                collection.write_frontier.clone_from(new_upper);
2928            }
2929
2930            let mut new_read_capability = collection
2931                .read_policy
2932                .frontier(collection.write_frontier.borrow());
2933
2934            if id.is_user() {
2935                trace!(
2936                    %id,
2937                    implied_capability = ?collection.implied_capability,
2938                    policy = ?collection.read_policy,
2939                    write_frontier = ?collection.write_frontier,
2940                    ?new_read_capability,
2941                    "update_write_frontiers");
2942            }
2943
2944            if PartialOrder::less_equal(&collection.implied_capability, &new_read_capability) {
2945                let mut update = ChangeBatch::new();
2946                update.extend(new_read_capability.iter().map(|time| (*time, 1)));
2947                std::mem::swap(&mut collection.implied_capability, &mut new_read_capability);
2948                update.extend(new_read_capability.iter().map(|time| (*time, -1)));
2949
2950                if !update.is_empty() {
2951                    read_capability_changes.insert(*id, update);
2952                }
2953            }
2954        }
2955
2956        if !read_capability_changes.is_empty() {
2957            StorageCollectionsImpl::update_read_capabilities_inner(
2958                &self.cmds_tx,
2959                &mut self_collections,
2960                &mut read_capability_changes,
2961            );
2962        }
2963    }
2964
2965    async fn downgrade_sinces(&mut self, cmds: BTreeMap<GlobalId, Antichain<Timestamp>>) {
2966        // Process all persist calls concurrently.
2967        let mut futures = Vec::with_capacity(cmds.len());
2968        for (id, new_since) in cmds {
2969            // We need to take the since handles here, to satisfy the borrow checker.
2970            // We make sure to always put them back below.
2971            let Some(mut since_handle) = self.since_handles.remove(&id) else {
2972                // This can happen when someone concurrently drops a collection.
2973                trace!("downgrade_sinces: reference to absent collection {id}");
2974                continue;
2975            };
2976
2977            let fut = async move {
2978                if id.is_user() {
2979                    trace!("downgrading since of {} to {:?}", id, new_since);
2980                }
2981
2982                let epoch = since_handle.opaque().clone();
2983                let result = if new_since.is_empty() {
2984                    // A shard's since reaching the empty frontier is a prereq for
2985                    // being able to finalize a shard, so the final downgrade should
2986                    // never be rate-limited.
2987                    Some(
2988                        since_handle
2989                            .compare_and_downgrade_since(&epoch, (&epoch, &new_since))
2990                            .await,
2991                    )
2992                } else {
2993                    since_handle
2994                        .maybe_compare_and_downgrade_since(&epoch, (&epoch, &new_since))
2995                        .await
2996                };
2997                (id, since_handle, result)
2998            };
2999            futures.push(fut);
3000        }
3001
3002        for (id, since_handle, result) in futures::future::join_all(futures).await {
3003            let new_since = match result {
3004                Some(Ok(since)) => Some(since),
3005                Some(Err(other_epoch)) => mz_ore::halt!(
3006                    "fenced by envd @ {other_epoch:?}. ours = {:?}",
3007                    since_handle.opaque(),
3008                ),
3009                None => None,
3010            };
3011
3012            self.since_handles.insert(id, since_handle);
3013
3014            if new_since.is_some_and(|s| s.is_empty()) {
3015                info!(%id, "removing persist handles because the since advanced to []!");
3016
3017                let _since_handle = self.since_handles.remove(&id).expect("known to exist");
3018                let Some(dropped_shard_id) = self.shard_by_id.remove(&id) else {
3019                    panic!("missing GlobalId -> ShardId mapping for id {id}");
3020                };
3021
3022                // We're not responsible for writes to tables, so we also don't
3023                // de-register them from the txn system. Whoever is responsible
3024                // will remove them. We only make sure to remove the table from
3025                // our tracking.
3026                self.txns_shards.remove(&id);
3027
3028                if self
3029                    .config
3030                    .lock()
3031                    .expect("lock poisoned")
3032                    .parameters
3033                    .finalize_shards
3034                {
3035                    info!(
3036                        %id, %dropped_shard_id,
3037                        "enqueuing shard finalization due to dropped collection and dropped \
3038                         persist handle",
3039                    );
3040                    self.finalizable_shards.lock().insert(dropped_shard_id);
3041                } else {
3042                    info!(
3043                        "not triggering shard finalization due to dropped storage object \
3044                         because enable_storage_shard_finalization parameter is false"
3045                    );
3046                }
3047            }
3048        }
3049    }
3050}
3051
3052struct FinalizeShardsTaskConfig {
3053    envd_epoch: NonZeroI64,
3054    config: Arc<Mutex<StorageConfiguration>>,
3055    metrics: StorageCollectionsMetrics,
3056    finalizable_shards: Arc<ShardIdSet>,
3057    finalized_shards: Arc<ShardIdSet>,
3058    persist_location: PersistLocation,
3059    persist: Arc<PersistClientCache>,
3060    read_only: bool,
3061}
3062
3063async fn finalize_shards_task(
3064    FinalizeShardsTaskConfig {
3065        envd_epoch,
3066        config,
3067        metrics,
3068        finalizable_shards,
3069        finalized_shards,
3070        persist_location,
3071        persist,
3072        read_only,
3073    }: FinalizeShardsTaskConfig,
3074) {
3075    if read_only {
3076        info!("disabling shard finalization in read only mode");
3077        return;
3078    }
3079
3080    let mut interval = tokio::time::interval(Duration::from_secs(5));
3081    interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
3082    loop {
3083        interval.tick().await;
3084
3085        if !config
3086            .lock()
3087            .expect("lock poisoned")
3088            .parameters
3089            .finalize_shards
3090        {
3091            debug!(
3092                "not triggering shard finalization due to dropped storage object because enable_storage_shard_finalization parameter is false"
3093            );
3094            continue;
3095        }
3096
3097        let current_finalizable_shards = {
3098            // We hold the lock for as short as possible and pull our cloned set
3099            // of shards.
3100            finalizable_shards.lock().iter().cloned().collect_vec()
3101        };
3102
3103        if current_finalizable_shards.is_empty() {
3104            debug!("no shards to finalize");
3105            continue;
3106        }
3107
3108        debug!(?current_finalizable_shards, "attempting to finalize shards");
3109
3110        // Open a persist client to delete unused shards.
3111        let persist_client = persist.open(persist_location.clone()).await.unwrap();
3112
3113        let metrics = &metrics;
3114        let finalizable_shards = &finalizable_shards;
3115        let finalized_shards = &finalized_shards;
3116        let persist_client = &persist_client;
3117        let diagnostics = &Diagnostics::from_purpose("finalizing shards");
3118
3119        let force_downgrade_since = STORAGE_DOWNGRADE_SINCE_DURING_FINALIZATION
3120            .get(config.lock().expect("lock poisoned").config_set());
3121
3122        let epoch = &PersistEpoch::from(envd_epoch);
3123
3124        futures::stream::iter(current_finalizable_shards.clone())
3125            .map(|shard_id| async move {
3126                let persist_client = persist_client.clone();
3127                let diagnostics = diagnostics.clone();
3128                let epoch = epoch.clone();
3129
3130                metrics.finalization_started.inc();
3131
3132                let is_finalized = persist_client
3133                    .is_finalized::<SourceData, (), Timestamp, StorageDiff>(shard_id, diagnostics)
3134                    .await
3135                    .expect("invalid persist usage");
3136
3137                if is_finalized {
3138                    debug!(%shard_id, "shard is already finalized!");
3139                    Some(shard_id)
3140                } else {
3141                    debug!(%shard_id, "finalizing shard");
3142                    let finalize = || async move {
3143                        // TODO: thread the global ID into the shard finalization WAL
3144                        let diagnostics = Diagnostics::from_purpose("finalizing shards");
3145
3146                        // We only use the writer to advance the upper, so using a dummy schema is
3147                        // fine.
3148                        let mut write_handle: WriteHandle<SourceData, (), Timestamp, StorageDiff> =
3149                            persist_client
3150                                .open_writer(
3151                                    shard_id,
3152                                    Arc::new(RelationDesc::empty()),
3153                                    Arc::new(UnitSchema),
3154                                    diagnostics,
3155                                )
3156                                .await
3157                                .expect("invalid persist usage");
3158                        write_handle.advance_upper(&Antichain::new()).await;
3159                        write_handle.expire().await;
3160
3161                        if force_downgrade_since {
3162                            let our_opaque = Opaque::encode(&epoch);
3163                            let mut since_handle: SinceHandle<
3164                                SourceData,
3165                                (),
3166                                Timestamp,
3167                                StorageDiff,
3168                            > = persist_client
3169                                .open_critical_since(
3170                                    shard_id,
3171                                    PersistClient::CONTROLLER_CRITICAL_SINCE,
3172                                    our_opaque.clone(),
3173                                    Diagnostics::from_purpose("finalizing shards"),
3174                                )
3175                                .await
3176                                .expect("invalid persist usage");
3177                            let handle_opaque = since_handle.opaque().clone();
3178                            let opaque = if our_opaque.codec_name() == handle_opaque.codec_name()
3179                                && epoch.0 > handle_opaque.decode::<PersistEpoch>().0
3180                            {
3181                                // We're newer, but it's fine to use the
3182                                // handle's old epoch to try and downgrade.
3183                                handle_opaque
3184                            } else {
3185                                // Good luck, buddy! The downgrade below will
3186                                // not succeed. There's a process with a newer
3187                                // epoch out there and someone at some juncture
3188                                // will fence out this process.
3189                                // TODO: consider applying the downgrade no matter what!
3190                                our_opaque
3191                            };
3192                            let new_since = Antichain::new();
3193                            let downgrade = since_handle
3194                                .compare_and_downgrade_since(&opaque, (&opaque, &new_since))
3195                                .await;
3196                            if let Err(e) = downgrade {
3197                                warn!("tried to finalize a shard with an advancing epoch: {e:?}");
3198                                return Ok(());
3199                            }
3200                            // Not available now, so finalization is broken.
3201                            // since_handle.expire().await;
3202                        }
3203
3204                        persist_client
3205                            .finalize_shard::<SourceData, (), Timestamp, StorageDiff>(
3206                                shard_id,
3207                                Diagnostics::from_purpose("finalizing shards"),
3208                            )
3209                            .await
3210                    };
3211
3212                    match finalize().await {
3213                        Err(e) => {
3214                            // Rather than error, just leave this shard as
3215                            // one to finalize later.
3216                            warn!("error during finalization of shard {shard_id}: {e:?}");
3217                            None
3218                        }
3219                        Ok(()) => {
3220                            debug!(%shard_id, "finalize success!");
3221                            Some(shard_id)
3222                        }
3223                    }
3224                }
3225            })
3226            // Poll each future for each collection concurrently, maximum of 10
3227            // at a time.
3228            // TODO(benesch): the concurrency here should be configurable
3229            // via LaunchDarkly.
3230            .buffer_unordered(10)
3231            // HERE BE DRAGONS: see warning on other uses of buffer_unordered.
3232            // The closure passed to `for_each` must remain fast or we risk
3233            // starving the finalization futures of calls to `poll`.
3234            .for_each(|shard_id| async move {
3235                match shard_id {
3236                    None => metrics.finalization_failed.inc(),
3237                    Some(shard_id) => {
3238                        // We make successfully finalized shards available for
3239                        // removal from the finalization WAL one by one, so that
3240                        // a handful of stuck shards don't prevent us from
3241                        // removing the shards that have made progress. The
3242                        // overhead of repeatedly acquiring and releasing the
3243                        // locks is negligible.
3244                        {
3245                            let mut finalizable_shards = finalizable_shards.lock();
3246                            let mut finalized_shards = finalized_shards.lock();
3247                            finalizable_shards.remove(&shard_id);
3248                            finalized_shards.insert(shard_id);
3249                        }
3250
3251                        metrics.finalization_succeeded.inc();
3252                    }
3253                }
3254            })
3255            .await;
3256
3257        debug!("done finalizing shards");
3258    }
3259}
3260
3261#[derive(Debug)]
3262pub(crate) enum SnapshotStatsAsOf {
3263    /// Stats for a shard with an "eager" upper (one that continually advances
3264    /// as time passes, even if no writes are coming in).
3265    Direct(Antichain<Timestamp>),
3266    /// Stats for a shard with a "lazy" upper (one that only physically advances
3267    /// in response to writes).
3268    Txns(DataSnapshot<Timestamp>),
3269}
3270
3271#[cfg(test)]
3272mod tests {
3273    use std::str::FromStr;
3274    use std::sync::Arc;
3275
3276    use mz_build_info::DUMMY_BUILD_INFO;
3277    use mz_dyncfg::ConfigSet;
3278    use mz_ore::assert_err;
3279    use mz_ore::metrics::{MetricsRegistry, UIntGauge};
3280    use mz_ore::now::SYSTEM_TIME;
3281    use mz_ore::url::SensitiveUrl;
3282    use mz_persist_client::cache::PersistClientCache;
3283    use mz_persist_client::cfg::PersistConfig;
3284    use mz_persist_client::rpc::PubSubClientConnection;
3285    use mz_persist_client::{Diagnostics, PersistClient, PersistLocation, ShardId};
3286    use mz_persist_types::codec_impls::UnitSchema;
3287    use mz_repr::{RelationDesc, Row};
3288    use mz_secrets::InMemorySecretsController;
3289
3290    use super::*;
3291
3292    #[mz_ore::test]
3293    fn test_partition_finalizable_shards() {
3294        let active_shard = ShardId::new();
3295        let dropped_shard = ShardId::new();
3296        let collection_metadata = BTreeMap::from([
3297            (GlobalId::User(1), active_shard),
3298            (GlobalId::User(2), active_shard),
3299            (GlobalId::User(3), dropped_shard),
3300        ]);
3301        let active_collection_ids = BTreeSet::from([GlobalId::User(1), GlobalId::User(2)]);
3302        let unfinalized_shards = BTreeSet::from([active_shard, dropped_shard]);
3303
3304        let (referenced_shards, finalizable_shards) = partition_finalizable_shards(
3305            collection_metadata,
3306            &active_collection_ids,
3307            unfinalized_shards,
3308        );
3309
3310        assert_eq!(referenced_shards, BTreeSet::from([active_shard]));
3311        assert_eq!(finalizable_shards, BTreeSet::from([dropped_shard]));
3312    }
3313
3314    #[mz_ore::test(tokio::test)]
3315    #[cfg_attr(miri, ignore)] // unsupported operation: integer-to-pointer casts and `ptr::from_exposed_addr`
3316    async fn test_snapshot_stats(&self) {
3317        let persist_location = PersistLocation {
3318            blob_uri: SensitiveUrl::from_str("mem://").expect("invalid URL"),
3319            consensus_uri: SensitiveUrl::from_str("mem://").expect("invalid URL"),
3320        };
3321        let persist_client = PersistClientCache::new(
3322            PersistConfig::new_default_configs(&DUMMY_BUILD_INFO, SYSTEM_TIME.clone()),
3323            &MetricsRegistry::new(),
3324            |_, _| PubSubClientConnection::noop(),
3325        );
3326        let persist_client = Arc::new(persist_client);
3327
3328        let (cmds_tx, mut background_task) =
3329            BackgroundTask::new_for_test(persist_location.clone(), Arc::clone(&persist_client));
3330        let background_task =
3331            mz_ore::task::spawn(|| "storage_collections::background_task", async move {
3332                background_task.run().await
3333            });
3334
3335        let persist = persist_client.open(persist_location).await.unwrap();
3336
3337        let shard_id = ShardId::new();
3338        let since_handle = persist
3339            .open_critical_since(
3340                shard_id,
3341                PersistClient::CONTROLLER_CRITICAL_SINCE,
3342                Opaque::encode(&PersistEpoch::default()),
3343                Diagnostics::for_tests(),
3344            )
3345            .await
3346            .unwrap();
3347        let write_handle = persist
3348            .open_writer::<SourceData, (), mz_repr::Timestamp, StorageDiff>(
3349                shard_id,
3350                Arc::new(RelationDesc::empty()),
3351                Arc::new(UnitSchema),
3352                Diagnostics::for_tests(),
3353            )
3354            .await
3355            .unwrap();
3356
3357        cmds_tx
3358            .send(BackgroundCmd::Register {
3359                id: GlobalId::User(1),
3360                is_in_txns: false,
3361                since_handle: SinceHandleWrapper::Critical(since_handle),
3362                write_handle,
3363            })
3364            .unwrap();
3365
3366        let mut write_handle = persist
3367            .open_writer::<SourceData, (), mz_repr::Timestamp, StorageDiff>(
3368                shard_id,
3369                Arc::new(RelationDesc::empty()),
3370                Arc::new(UnitSchema),
3371                Diagnostics::for_tests(),
3372            )
3373            .await
3374            .unwrap();
3375
3376        // No stats for unknown GlobalId.
3377        let stats =
3378            snapshot_stats(&cmds_tx, GlobalId::User(2), Antichain::from_elem(0.into())).await;
3379        assert_err!(stats);
3380
3381        // Stats don't resolve for as_of past the upper.
3382        let stats_fut = snapshot_stats(&cmds_tx, GlobalId::User(1), Antichain::from_elem(1.into()));
3383        assert_none!(stats_fut.now_or_never());
3384
3385        // // Call it again because now_or_never consumed our future and it's not clone-able.
3386        let stats_ts1_fut =
3387            snapshot_stats(&cmds_tx, GlobalId::User(1), Antichain::from_elem(1.into()));
3388
3389        // Write some data.
3390        let data = (
3391            (SourceData(Ok(Row::default())), ()),
3392            mz_repr::Timestamp::from(0),
3393            1i64,
3394        );
3395        let () = write_handle
3396            .compare_and_append(
3397                &[data],
3398                Antichain::from_elem(0.into()),
3399                Antichain::from_elem(1.into()),
3400            )
3401            .await
3402            .unwrap()
3403            .unwrap();
3404
3405        // Verify that we can resolve stats for ts 0 while the ts 1 stats call is outstanding.
3406        let stats = snapshot_stats(&cmds_tx, GlobalId::User(1), Antichain::from_elem(0.into()))
3407            .await
3408            .unwrap();
3409        assert_eq!(stats.num_updates, 1);
3410
3411        // Write more data and unblock the ts 1 call
3412        let data = (
3413            (SourceData(Ok(Row::default())), ()),
3414            mz_repr::Timestamp::from(1),
3415            1i64,
3416        );
3417        let () = write_handle
3418            .compare_and_append(
3419                &[data],
3420                Antichain::from_elem(1.into()),
3421                Antichain::from_elem(2.into()),
3422            )
3423            .await
3424            .unwrap()
3425            .unwrap();
3426
3427        let stats = stats_ts1_fut.await.unwrap();
3428        assert_eq!(stats.num_updates, 2);
3429
3430        // Make sure it runs until at least here.
3431        drop(background_task);
3432    }
3433
3434    async fn snapshot_stats(
3435        cmds_tx: &mpsc::UnboundedSender<BackgroundCmd>,
3436        id: GlobalId,
3437        as_of: Antichain<Timestamp>,
3438    ) -> Result<SnapshotStats, StorageError> {
3439        let (tx, rx) = oneshot::channel();
3440        cmds_tx
3441            .send(BackgroundCmd::SnapshotStats(
3442                id,
3443                SnapshotStatsAsOf::Direct(as_of),
3444                tx,
3445            ))
3446            .unwrap();
3447        let res = rx.await.expect("BackgroundTask should be live").0;
3448
3449        res.await
3450    }
3451
3452    impl BackgroundTask {
3453        fn new_for_test(
3454            _persist_location: PersistLocation,
3455            _persist_client: Arc<PersistClientCache>,
3456        ) -> (mpsc::UnboundedSender<BackgroundCmd>, Self) {
3457            let (cmds_tx, cmds_rx) = mpsc::unbounded_channel();
3458            let (_holds_tx, holds_rx) = mpsc::unbounded_channel();
3459            let connection_context =
3460                ConnectionContext::for_tests(Arc::new(InMemorySecretsController::new()));
3461
3462            let task = Self {
3463                config: Arc::new(Mutex::new(StorageConfiguration::new(
3464                    connection_context,
3465                    ConfigSet::default(),
3466                ))),
3467                cmds_tx: cmds_tx.clone(),
3468                cmds_rx,
3469                holds_rx,
3470                finalizable_shards: Arc::new(ShardIdSet::new(
3471                    UIntGauge::new("finalizable_shards", "dummy gauge for tests").unwrap(),
3472                )),
3473                collections: Arc::new(Mutex::new(BTreeMap::new())),
3474                shard_by_id: BTreeMap::new(),
3475                since_handles: BTreeMap::new(),
3476                txns_handle: None,
3477                txns_shards: BTreeSet::new(),
3478            };
3479
3480            (cmds_tx, task)
3481        }
3482    }
3483}