1use 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#[async_trait]
85pub trait StorageCollections: Debug + Sync {
86 async fn initialize_state(
93 &self,
94 txn: &mut (dyn StorageTxn + Send),
95 init_ids: BTreeSet<GlobalId>,
96 ) -> Result<(), StorageError>;
97
98 fn update_parameters(&self, config_params: StorageParameters);
100
101 fn collection_metadata(&self, id: GlobalId) -> Result<CollectionMetadata, CollectionMissing>;
103
104 fn active_collection_metadatas(&self) -> Vec<(GlobalId, CollectionMetadata)>;
110
111 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 fn collections_frontiers(
123 &self,
124 id: Vec<GlobalId>,
125 ) -> Result<Vec<CollectionFrontiers>, CollectionMissing>;
126
127 fn active_collection_frontiers(&self) -> Vec<CollectionFrontiers>;
132
133 fn check_exists(&self, id: GlobalId) -> Result<(), StorageError>;
136
137 async fn snapshot_stats(
140 &self,
141 id: GlobalId,
142 as_of: Antichain<Timestamp>,
143 ) -> Result<SnapshotStats, StorageError>;
144
145 async fn snapshot_parts_stats(
154 &self,
155 id: GlobalId,
156 as_of: Antichain<Timestamp>,
157 ) -> BoxFuture<'static, Result<SnapshotPartsStats, StorageError>>;
158
159 fn snapshot(
161 &self,
162 id: GlobalId,
163 as_of: Timestamp,
164 ) -> BoxFuture<'static, Result<Vec<(Row, StorageDiff)>, StorageError>>;
165
166 async fn snapshot_latest(&self, id: GlobalId) -> Result<Vec<Row>, StorageError>;
181
182 fn snapshot_cursor(
184 &self,
185 id: GlobalId,
186 as_of: Timestamp,
187 ) -> BoxFuture<'static, Result<SnapshotCursor, StorageError>>;
188
189 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 fn create_update_builder(
205 &self,
206 id: GlobalId,
207 ) -> BoxFuture<
208 'static,
209 Result<TimestamplessUpdateBuilder<SourceData, (), StorageDiff>, StorageError>,
210 >;
211
212 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 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 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 fn drop_collections_unvalidated(
279 &self,
280 storage_metadata: &StorageMetadata,
281 identifiers: Vec<GlobalId>,
282 );
283
284 fn set_read_policies(&self, policies: Vec<(GlobalId, ReadPolicy)>);
298
299 fn acquire_read_holds(
302 &self,
303 desired_holds: Vec<GlobalId>,
304 ) -> Result<Vec<ReadHold>, CollectionMissing>;
305
306 fn determine_time_dependence(
309 &self,
310 id: GlobalId,
311 ) -> Result<Option<TimeDependence>, TimeDependenceError>;
312
313 fn dump(&self) -> Result<serde_json::Value, anyhow::Error>;
315}
316
317pub struct SnapshotCursor {
320 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#[derive(Debug)]
337pub struct CollectionFrontiers {
338 pub id: GlobalId,
340
341 pub write_frontier: Antichain<Timestamp>,
343
344 pub implied_capability: Antichain<Timestamp>,
351
352 pub read_capabilities: Antichain<Timestamp>,
355}
356
357#[derive(Debug, Clone)]
360pub struct StorageCollectionsImpl {
361 envd_epoch: NonZeroI64,
364
365 read_only: bool,
371
372 finalizable_shards: Arc<ShardIdSet>,
375
376 finalized_shards: Arc<ShardIdSet>,
381
382 collections: Arc<std::sync::Mutex<BTreeMap<GlobalId, CollectionState>>>,
384
385 txns_read: TxnsRead<Timestamp>,
387
388 config: Arc<Mutex<StorageConfiguration>>,
390
391 initial_txn_upper: Antichain<Timestamp>,
400
401 persist_location: PersistLocation,
403
404 persist: Arc<PersistClientCache>,
406
407 cmd_tx: mpsc::UnboundedSender<BackgroundCmd>,
409
410 holds_tx: mpsc::UnboundedSender<(GlobalId, ChangeBatch<Timestamp>)>,
412
413 _background_task: Arc<AbortOnDropHandle<()>>,
415 _finalize_shards_task: Arc<AbortOnDropHandle<()>>,
416}
417
418type SourceDataStream = BoxStream<'static, (SourceData, Timestamp, StorageDiff)>;
427
428impl StorageCollectionsImpl {
431 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 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 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 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 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 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 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 write_handle.fetch_recent_upper().await;
628
629 (write_handle, since_handle)
630 }
631
632 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 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 let since_handle = {
687 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 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 let unchecked_success = current_epoch.0.map(|e| e <= our_epoch).unwrap_or(true);
715
716 if unchecked_success {
717 let checked_success = handle
720 .compare_and_downgrade_since(
721 &Opaque::encode(¤t_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 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 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 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 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 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 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 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 #[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 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 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 let diagnostics = Diagnostics {
989 shard_name: id.to_string(),
990 handle_purpose: format!("controller data for {}", id),
991 };
992 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 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 read_handle
1058 .snapshot_and_fetch(Antichain::from_elem(as_of))
1059 .await
1060 }
1061 Some(txns_read) => {
1062 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 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 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 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 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 let mut collections_net = BTreeMap::new();
1201
1202 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;
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 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 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 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
1312fn 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#[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 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 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 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 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 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 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 assert_eq!(diff, 1, "snapshot doesn't accumulate to set");
1562 row
1563 })
1564 .collect()
1565 }
1566 Some(_min) => {
1567 Vec::new()
1569 }
1570 _ => {
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 assert_eq!(txns_id, self.txns_read.txns_id());
1596 self.txns_read.clone()
1597 });
1598 let persist = Arc::clone(&self.persist);
1599
1600 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 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 let dropped_mappings = txn.delete_collection_metadata(ids_to_drop);
1722
1723 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 let finalized_shards = self.finalized_shards.lock().iter().copied().collect();
1752 txn.remove_unfinalized_shards(finalized_shards);
1753
1754 Ok(())
1755 }
1756
1757 #[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 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 let enriched_with_metadata = collections
1787 .into_iter()
1788 .map(|(id, description)| {
1789 let data_shard = storage_metadata.get_collection_shard(id)?;
1790
1791 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 let persist_client = self
1812 .persist
1813 .open(self.persist_location.clone())
1814 .await
1815 .unwrap();
1816 let persist_client = &persist_client;
1817 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 debug!("mapping GlobalId={} to shard ({})", id, metadata.data_shard);
1831
1832 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 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 .buffer_unordered(50)
1891 .try_collect()
1905 .await?;
1906
1907 #[derive(Ord, PartialOrd, Eq, PartialEq)]
1909 enum DependencyOrder {
1910 Table(Reverse<GlobalId>),
1912 Collection(GlobalId),
1914 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 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 let storage_dependencies =
1933 Self::determine_collection_dependencies(&*self_collections, id, &description)?;
1934
1935 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 PartialOrder::less_than(&data_shard_since, &dependency_since) {
1958 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 let time_dependence = {
2003 use DataSource::*;
2004 if let Some(timeline) = &description.timeline
2005 && *timeline != Timeline::EpochMilliseconds
2006 {
2007 None
2009 } else {
2010 match &description.data_source {
2011 Ingestion(ingestion) => {
2012 use GenericSourceConnection::*;
2013 match ingestion.desc.connection {
2014 Kafka(_) | Postgres(_) | MySql(_) | SqlServer(_) => {
2017 Some(TimeDependence::default())
2018 }
2019 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(_) | Progress | Table { .. } | Webhook { .. } => {
2029 Some(TimeDependence::default())
2030 }
2031 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 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 if is_in_txns(id, &metadata)
2068 && PartialOrder::less_than(
2069 &collection_state.write_frontier,
2070 &self.initial_txn_upper,
2071 )
2072 {
2073 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 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 let diagnostics = Diagnostics {
2132 shard_name: existing_collection.to_string(),
2133 handle_purpose: "alter_table_desc".to_string(),
2134 };
2135 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 CaESchema::Ok(_id) => {}
2157 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 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 {
2201 let mut self_collections = self.collections.lock().expect("lock poisoned");
2202
2203 let existing = self_collections
2205 .get_mut(&existing_collection)
2206 .expect("existing collection missing");
2207
2208 assert_none!(existing.primary);
2210
2211 existing.primary = Some(new_collection);
2213 existing.storage_dependencies.push(new_collection);
2214
2215 let implied_capability = existing.read_capabilities.frontier().to_owned();
2219 let write_frontier = existing.write_frontier.clone();
2220
2221 let mut changes = ChangeBatch::new();
2228 changes.extend(implied_capability.iter().map(|t| (*t, 1)));
2229
2230 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 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 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 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 let mut finalized_policies = Vec::new();
2297
2298 for id in identifiers {
2299 let Some(collection) = self_collections.get(&id) else {
2301 continue;
2302 };
2303
2304 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 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 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 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 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#[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 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#[derive(Debug, Clone)]
2604struct CollectionState {
2605 primary: Option<GlobalId>,
2612
2613 time_dependence: Option<TimeDependence>,
2615 ingestion_remap_collection_id: Option<GlobalId>,
2617
2618 pub read_capabilities: MutableAntichain<Timestamp>,
2624
2625 pub implied_capability: Antichain<Timestamp>,
2629
2630 pub read_policy: ReadPolicy,
2632
2633 pub storage_dependencies: Vec<GlobalId>,
2635
2636 pub write_frontier: Antichain<Timestamp>,
2638
2639 pub collection_metadata: CollectionMetadata,
2640}
2641
2642impl CollectionState {
2643 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 pub fn is_dropped(&self) -> bool {
2673 self.read_capabilities.is_empty()
2674 }
2675}
2676
2677#[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 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
2714pub(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 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 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 handle.expire().await;
2798 }
2799 }
2800 }
2801 cmd = self.cmds_rx.recv() => {
2802 let Some(cmd) = cmd else {
2803 break;
2805 };
2806
2807 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 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 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 let mut futures = Vec::with_capacity(cmds.len());
2980 for (id, new_since) in cmds {
2981 let Some(mut since_handle) = self.since_handles.remove(&id) else {
2984 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 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 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 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 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 let diagnostics = Diagnostics::from_purpose("finalizing shards");
3157
3158 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 handle_opaque
3196 } else {
3197 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 }
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 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 .buffer_unordered(10)
3243 .for_each(|shard_id| async move {
3247 match shard_id {
3248 None => metrics.finalization_failed.inc(),
3249 Some(shard_id) => {
3250 {
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 Direct(Antichain<Timestamp>),
3278 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)] 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 let stats =
3390 snapshot_stats(&cmds_tx, GlobalId::User(2), Antichain::from_elem(0.into())).await;
3391 assert_err!(stats);
3392
3393 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 let stats_ts1_fut =
3399 snapshot_stats(&cmds_tx, GlobalId::User(1), Antichain::from_elem(1.into()));
3400
3401 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 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 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 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}