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