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 dependency_violations = {
1926 let mut self_collections = self.collections.lock().expect("lock poisoned");
1929 let mut dependency_violations = Vec::new();
1930
1931 for (id, description, write_handle, since_handle, metadata) in to_register {
1932 let write_frontier = write_handle.upper();
1933 let data_shard_since = since_handle.since().clone();
1934
1935 let storage_dependencies =
1937 Self::determine_collection_dependencies(&*self_collections, id, &description)?;
1938
1939 let initial_since = match storage_dependencies
1941 .iter()
1942 .at_most_one()
1943 .expect("should have at most one dependency")
1944 {
1945 Some(dep) => {
1946 let dependency_collection = self_collections
1947 .get(dep)
1948 .ok_or(StorageError::IdentifierMissing(*dep))?;
1949 let dependency_since = dependency_collection.implied_capability.clone();
1950
1951 if PartialOrder::less_than(&data_shard_since, &dependency_since) {
1962 if description.primary.is_none()
1984 && write_frontier.elements() != &[Timestamp::MIN]
1985 && !write_frontier.is_empty()
1986 && !PartialOrder::less_than(&dependency_since, write_frontier)
1987 {
1988 dependency_violations.push(DependencyViolation {
1989 id,
1990 dependency: *dep,
1991 data_shard_since: data_shard_since.clone(),
1992 write_frontier: write_frontier.clone(),
1993 dependency_since: dependency_since.clone(),
1994 ingestion_export: matches!(
1995 description.data_source,
1996 DataSource::IngestionExport { .. }
1997 )
1998 .then(|| metadata.clone()),
1999 });
2000 }
2001
2002 dependency_since
2003 } else {
2004 data_shard_since
2005 }
2006 }
2007 None => data_shard_since,
2008 };
2009
2010 let time_dependence = {
2012 use DataSource::*;
2013 if let Some(timeline) = &description.timeline
2014 && *timeline != Timeline::EpochMilliseconds
2015 {
2016 None
2018 } else {
2019 match &description.data_source {
2020 Ingestion(ingestion) => {
2021 use GenericSourceConnection::*;
2022 match ingestion.desc.connection {
2023 Kafka(_) | Postgres(_) | MySql(_) | SqlServer(_) => {
2026 Some(TimeDependence::default())
2027 }
2028 LoadGenerator(_) => None,
2030 }
2031 }
2032 IngestionExport { ingestion_id, .. } => {
2033 let c = self_collections.get(ingestion_id).expect("known to exist");
2034 c.time_dependence.clone()
2035 }
2036 Introspection(_) | Progress | Table { .. } | Webhook { .. } => {
2038 Some(TimeDependence::default())
2039 }
2040 Other => None,
2042 Sink { .. } => None,
2043 }
2044 }
2045 };
2046
2047 let ingestion_remap_collection_id = match &description.data_source {
2048 DataSource::Ingestion(desc) => Some(desc.remap_collection_id),
2049 _ => None,
2050 };
2051
2052 let mut collection_state = CollectionState::new(
2053 description.primary,
2054 time_dependence,
2055 ingestion_remap_collection_id,
2056 initial_since,
2057 write_frontier.clone(),
2058 storage_dependencies,
2059 metadata.clone(),
2060 );
2061
2062 match &description.data_source {
2064 DataSource::Introspection(_) => {
2065 self_collections.insert(id, collection_state);
2066 }
2067 DataSource::Webhook => {
2068 self_collections.insert(id, collection_state);
2069 }
2070 DataSource::IngestionExport { .. } => {
2071 self_collections.insert(id, collection_state);
2072 }
2073 DataSource::Table => {
2074 if is_in_txns(id, &metadata)
2077 && PartialOrder::less_than(
2078 &collection_state.write_frontier,
2079 &self.initial_txn_upper,
2080 )
2081 {
2082 collection_state
2088 .write_frontier
2089 .clone_from(&self.initial_txn_upper);
2090 }
2091 self_collections.insert(id, collection_state);
2092 }
2093 DataSource::Progress | DataSource::Other => {
2094 self_collections.insert(id, collection_state);
2095 }
2096 DataSource::Ingestion(_) => {
2097 self_collections.insert(id, collection_state);
2098 }
2099 DataSource::Sink { .. } => {
2100 self_collections.insert(id, collection_state);
2101 }
2102 }
2103
2104 self.register_handles(id, is_in_txns(id, &metadata), since_handle, write_handle);
2105
2106 self.install_collection_dependency_read_holds_inner(&mut *self_collections, id)?;
2108 }
2109 dependency_violations
2110 };
2111
2112 for violation in dependency_violations {
2113 if let Some(metadata) = &violation.ingestion_export
2128 && shard_has_no_updates(persist_client, violation.id, metadata).await
2129 {
2130 continue;
2131 }
2132 let DependencyViolation {
2133 id,
2134 dependency,
2135 data_shard_since,
2136 write_frontier,
2137 dependency_since,
2138 ingestion_export: _,
2139 } = violation;
2140 mz_ore::soft_panic_or_log!(
2141 "dependency ({dependency}) since has advanced past dependent ({id}) upper \n
2142 dependent ({id}): since {:?}, upper {:?} \n
2143 dependency ({dependency}): since {:?}",
2144 data_shard_since,
2145 write_frontier,
2146 dependency_since
2147 );
2148 }
2149
2150 self.synchronize_finalized_shards(storage_metadata);
2151
2152 Ok(())
2153 }
2154
2155 async fn alter_table_desc(
2156 &self,
2157 existing_collection: GlobalId,
2158 new_collection: GlobalId,
2159 new_desc: RelationDesc,
2160 expected_version: RelationVersion,
2161 ) -> Result<(), StorageError> {
2162 let data_shard = {
2163 let self_collections = self.collections.lock().expect("lock poisoned");
2164 let existing = self_collections
2165 .get(&existing_collection)
2166 .ok_or_else(|| StorageError::IdentifierMissing(existing_collection))?;
2167
2168 existing.collection_metadata.data_shard
2169 };
2170
2171 let persist_client = self
2172 .persist
2173 .open(self.persist_location.clone())
2174 .await
2175 .unwrap();
2176
2177 let diagnostics = Diagnostics {
2179 shard_name: existing_collection.to_string(),
2180 handle_purpose: "alter_table_desc".to_string(),
2181 };
2182 let expected_schema = expected_version.into();
2184 let schema_result = persist_client
2185 .compare_and_evolve_schema::<SourceData, (), Timestamp, StorageDiff>(
2186 data_shard,
2187 expected_schema,
2188 &new_desc,
2189 &UnitSchema,
2190 diagnostics,
2191 )
2192 .await
2193 .map_err(|e| StorageError::InvalidUsage(e.to_string()))?;
2194 tracing::info!(
2195 ?existing_collection,
2196 ?new_collection,
2197 ?new_desc,
2198 "evolved schema"
2199 );
2200
2201 match schema_result {
2202 CaESchema::Ok(_id) => {}
2204 CaESchema::ExpectedMismatch {
2209 schema_id,
2210 key,
2211 val,
2212 } => {
2213 mz_ore::soft_panic_or_log!(
2214 "schema expectation mismatch, expected {expected_schema:?} \
2215 but found {schema_id:?}, {key:?}, {val:?}"
2216 );
2217 return Err(StorageError::Generic(anyhow::anyhow!(
2218 "schema expected mismatch, {existing_collection:?}",
2219 )));
2220 }
2221 CaESchema::Incompatible => {
2222 mz_ore::soft_panic_or_log!(
2223 "incompatible schema! {existing_collection} {new_desc:?}"
2224 );
2225 return Err(StorageError::Generic(anyhow::anyhow!(
2226 "schema incompatible, {existing_collection:?}"
2227 )));
2228 }
2229 };
2230
2231 let (write_handle, since_handle) = self
2233 .open_data_handles(
2234 &new_collection,
2235 data_shard,
2236 None,
2237 new_desc.clone(),
2238 &persist_client,
2239 )
2240 .await;
2241
2242 {
2248 let mut self_collections = self.collections.lock().expect("lock poisoned");
2249
2250 let existing = self_collections
2252 .get_mut(&existing_collection)
2253 .expect("existing collection missing");
2254
2255 assert_none!(existing.primary);
2257
2258 existing.primary = Some(new_collection);
2260 existing.storage_dependencies.push(new_collection);
2261
2262 let implied_capability = existing.read_capabilities.frontier().to_owned();
2266 let write_frontier = existing.write_frontier.clone();
2267
2268 let mut changes = ChangeBatch::new();
2275 changes.extend(implied_capability.iter().map(|t| (*t, 1)));
2276
2277 let collection_meta = CollectionMetadata {
2279 persist_location: self.persist_location.clone(),
2280 relation_desc: new_desc.clone(),
2281 data_shard,
2282 txns_shard: Some(self.txns_read.txns_id().clone()),
2283 };
2284 let collection_state = CollectionState::new(
2285 None,
2286 existing.time_dependence.clone(),
2287 existing.ingestion_remap_collection_id.clone(),
2288 implied_capability,
2289 write_frontier,
2290 Vec::new(),
2291 collection_meta,
2292 );
2293
2294 self_collections.insert(new_collection, collection_state);
2296
2297 let mut updates = BTreeMap::from([(new_collection, changes)]);
2298 StorageCollectionsImpl::update_read_capabilities_inner(
2299 &self.cmd_tx,
2300 &mut *self_collections,
2301 &mut updates,
2302 );
2303 };
2304
2305 self.register_handles(new_collection, true, since_handle, write_handle);
2307
2308 info!(%existing_collection, %new_collection, ?new_desc, "altered table");
2309
2310 Ok(())
2311 }
2312
2313 fn drop_collections_unvalidated(
2314 &self,
2315 storage_metadata: &StorageMetadata,
2316 identifiers: Vec<GlobalId>,
2317 ) {
2318 debug!(?identifiers, "drop_collections_unvalidated");
2319
2320 let mut self_collections = self.collections.lock().expect("lock poisoned");
2321 let dropping: BTreeSet<_> = identifiers.iter().copied().collect();
2324 let active_collection_ids: BTreeSet<_> = self_collections
2325 .iter()
2326 .filter_map(|(id, collection)| {
2327 (!dropping.contains(id) && !collection.is_dropped()).then_some(*id)
2328 })
2329 .collect();
2330 let shards_in_use: BTreeSet<_> = storage_metadata
2331 .collection_metadata
2332 .iter()
2333 .filter_map(|(id, shard)| active_collection_ids.contains(id).then_some(*shard))
2334 .collect();
2335
2336 let mut finalized_policies = Vec::new();
2344
2345 for id in identifiers {
2346 let Some(collection) = self_collections.get(&id) else {
2348 continue;
2349 };
2350
2351 if collection.primary.is_none() {
2354 let metadata = storage_metadata.get_collection_shard(id);
2355 mz_ore::soft_assert_or_log!(
2356 matches!(metadata, Err(StorageError::IdentifierMissing(_))),
2357 "dropping {id}, but drop was not synchronized with storage \
2358 controller via `prepare_state`"
2359 );
2360
2361 let data_shard = collection.collection_metadata.data_shard;
2365 if shards_in_use.contains(&data_shard) {
2366 mz_ore::soft_panic_or_log!(
2367 "dropping {id} would release the since of shard {data_shard}, \
2368 which an active collection still uses"
2369 );
2370 continue;
2371 }
2372 }
2373
2374 finalized_policies.push((id, ReadPolicy::ValidFrom(Antichain::new())));
2375 }
2376
2377 self.set_read_policies_inner(&mut self_collections, finalized_policies);
2378
2379 drop(self_collections);
2380
2381 self.synchronize_finalized_shards(storage_metadata);
2382 }
2383
2384 fn set_read_policies(&self, policies: Vec<(GlobalId, ReadPolicy)>) {
2385 let mut collections = self.collections.lock().expect("lock poisoned");
2386
2387 if tracing::enabled!(tracing::Level::TRACE) {
2388 let user_capabilities = collections
2389 .iter_mut()
2390 .filter(|(id, _c)| id.is_user())
2391 .map(|(id, c)| {
2392 let updates = c.read_capabilities.updates().cloned().collect_vec();
2393 (*id, c.implied_capability.clone(), updates)
2394 })
2395 .collect_vec();
2396
2397 trace!(?policies, ?user_capabilities, "set_read_policies");
2398 }
2399
2400 self.set_read_policies_inner(&mut collections, policies);
2401
2402 if tracing::enabled!(tracing::Level::TRACE) {
2403 let user_capabilities = collections
2404 .iter_mut()
2405 .filter(|(id, _c)| id.is_user())
2406 .map(|(id, c)| {
2407 let updates = c.read_capabilities.updates().cloned().collect_vec();
2408 (*id, c.implied_capability.clone(), updates)
2409 })
2410 .collect_vec();
2411
2412 trace!(?user_capabilities, "after! set_read_policies");
2413 }
2414 }
2415
2416 fn acquire_read_holds(
2417 &self,
2418 desired_holds: Vec<GlobalId>,
2419 ) -> Result<Vec<ReadHold>, CollectionMissing> {
2420 if desired_holds.is_empty() {
2421 return Ok(vec![]);
2422 }
2423
2424 let mut collections = self.collections.lock().expect("lock poisoned");
2425
2426 let mut advanced_holds = Vec::new();
2427 for id in desired_holds.iter() {
2438 let collection = collections.get(id).ok_or(CollectionMissing(*id))?;
2439 let since = collection.read_capabilities.frontier().to_owned();
2440 advanced_holds.push((*id, since));
2441 }
2442
2443 let mut updates = advanced_holds
2444 .iter()
2445 .map(|(id, hold)| {
2446 let mut changes = ChangeBatch::new();
2447 changes.extend(hold.iter().map(|time| (*time, 1)));
2448 (*id, changes)
2449 })
2450 .collect::<BTreeMap<_, _>>();
2451
2452 StorageCollectionsImpl::update_read_capabilities_inner(
2453 &self.cmd_tx,
2454 &mut collections,
2455 &mut updates,
2456 );
2457
2458 let acquired_holds = advanced_holds
2459 .into_iter()
2460 .map(|(id, since)| ReadHold::with_channel(id, since, self.holds_tx.clone()))
2461 .collect_vec();
2462
2463 trace!(?desired_holds, ?acquired_holds, "acquire_read_holds");
2464
2465 Ok(acquired_holds)
2466 }
2467
2468 fn determine_time_dependence(
2470 &self,
2471 id: GlobalId,
2472 ) -> Result<Option<TimeDependence>, TimeDependenceError> {
2473 use TimeDependenceError::CollectionMissing;
2474 let collections = self.collections.lock().expect("lock poisoned");
2475 let state = collections.get(&id).ok_or(CollectionMissing(id))?;
2476 Ok(state.time_dependence.clone())
2477 }
2478
2479 fn dump(&self) -> Result<serde_json::Value, anyhow::Error> {
2480 let Self {
2482 envd_epoch,
2483 read_only,
2484 finalizable_shards,
2485 finalized_shards,
2486 collections,
2487 txns_read: _,
2488 config,
2489 initial_txn_upper,
2490 persist_location,
2491 persist: _,
2492 cmd_tx: _,
2493 holds_tx: _,
2494 _background_task: _,
2495 _finalize_shards_task: _,
2496 } = self;
2497
2498 let finalizable_shards: Vec<_> = finalizable_shards
2499 .lock()
2500 .iter()
2501 .map(ToString::to_string)
2502 .collect();
2503 let finalized_shards: Vec<_> = finalized_shards
2504 .lock()
2505 .iter()
2506 .map(ToString::to_string)
2507 .collect();
2508 let collections: BTreeMap<_, _> = collections
2509 .lock()
2510 .expect("poisoned")
2511 .iter()
2512 .map(|(id, c)| (id.to_string(), format!("{c:?}")))
2513 .collect();
2514 let config = format!("{:?}", config.lock().expect("poisoned"));
2515
2516 Ok(serde_json::json!({
2517 "envd_epoch": envd_epoch,
2518 "read_only": read_only,
2519 "finalizable_shards": finalizable_shards,
2520 "finalized_shards": finalized_shards,
2521 "collections": collections,
2522 "config": config,
2523 "initial_txn_upper": initial_txn_upper,
2524 "persist_location": format!("{persist_location:?}"),
2525 }))
2526 }
2527}
2528
2529#[derive(Debug)]
2536enum SinceHandleWrapper {
2537 Critical(SinceHandle<SourceData, (), Timestamp, StorageDiff>),
2538 Leased(ReadHandle<SourceData, (), Timestamp, StorageDiff>),
2539}
2540
2541impl SinceHandleWrapper {
2542 pub fn since(&self) -> &Antichain<Timestamp> {
2543 match self {
2544 Self::Critical(handle) => handle.since(),
2545 Self::Leased(handle) => handle.since(),
2546 }
2547 }
2548
2549 pub fn opaque(&self) -> PersistEpoch {
2550 match self {
2551 Self::Critical(handle) => handle.opaque().decode(),
2552 Self::Leased(_handle) => {
2553 PersistEpoch(None)
2558 }
2559 }
2560 }
2561
2562 pub async fn compare_and_downgrade_since(
2563 &mut self,
2564 expected: &PersistEpoch,
2565 (opaque, since): (&PersistEpoch, &Antichain<Timestamp>),
2566 ) -> Result<Antichain<Timestamp>, PersistEpoch> {
2567 match self {
2568 Self::Critical(handle) => handle
2569 .compare_and_downgrade_since(
2570 &Opaque::encode(expected),
2571 (&Opaque::encode(opaque), since),
2572 )
2573 .await
2574 .map_err(|e| e.decode()),
2575 Self::Leased(handle) => {
2576 assert_none!(opaque.0);
2577
2578 handle.downgrade_since(since).await;
2579
2580 Ok(since.clone())
2581 }
2582 }
2583 }
2584
2585 pub async fn maybe_compare_and_downgrade_since(
2586 &mut self,
2587 expected: &PersistEpoch,
2588 (opaque, since): (&PersistEpoch, &Antichain<Timestamp>),
2589 ) -> Option<Result<Antichain<Timestamp>, PersistEpoch>> {
2590 match self {
2591 Self::Critical(handle) => handle
2592 .maybe_compare_and_downgrade_since(
2593 &Opaque::encode(expected),
2594 (&Opaque::encode(opaque), since),
2595 )
2596 .await
2597 .map(|r| r.map_err(|o| o.decode())),
2598 Self::Leased(handle) => {
2599 assert_none!(opaque.0);
2600
2601 handle.maybe_downgrade_since(since).await;
2602
2603 Some(Ok(since.clone()))
2604 }
2605 }
2606 }
2607
2608 pub fn snapshot_stats(
2609 &self,
2610 id: GlobalId,
2611 as_of: Option<Antichain<Timestamp>>,
2612 ) -> BoxFuture<'static, Result<SnapshotStats, StorageError>> {
2613 match self {
2614 Self::Critical(handle) => {
2615 let res = handle
2616 .snapshot_stats(as_of)
2617 .map(move |x| x.map_err(|_| StorageError::ReadBeforeSince(id)));
2618 Box::pin(res)
2619 }
2620 Self::Leased(handle) => {
2621 let res = handle
2622 .snapshot_stats(as_of)
2623 .map(move |x| x.map_err(|_| StorageError::ReadBeforeSince(id)));
2624 Box::pin(res)
2625 }
2626 }
2627 }
2628
2629 pub fn snapshot_stats_from_txn(
2630 &self,
2631 id: GlobalId,
2632 data_snapshot: DataSnapshot<Timestamp>,
2633 ) -> BoxFuture<'static, Result<SnapshotStats, StorageError>> {
2634 match self {
2635 Self::Critical(handle) => Box::pin(
2636 data_snapshot
2637 .snapshot_stats_from_critical(handle)
2638 .map(move |x| x.map_err(|_| StorageError::ReadBeforeSince(id))),
2639 ),
2640 Self::Leased(handle) => Box::pin(
2641 data_snapshot
2642 .snapshot_stats_from_leased(handle)
2643 .map(move |x| x.map_err(|_| StorageError::ReadBeforeSince(id))),
2644 ),
2645 }
2646 }
2647}
2648
2649#[derive(Debug, Clone)]
2651struct CollectionState {
2652 primary: Option<GlobalId>,
2659
2660 time_dependence: Option<TimeDependence>,
2662 ingestion_remap_collection_id: Option<GlobalId>,
2664
2665 pub read_capabilities: MutableAntichain<Timestamp>,
2671
2672 pub implied_capability: Antichain<Timestamp>,
2676
2677 pub read_policy: ReadPolicy,
2679
2680 pub storage_dependencies: Vec<GlobalId>,
2682
2683 pub write_frontier: Antichain<Timestamp>,
2685
2686 pub collection_metadata: CollectionMetadata,
2687}
2688
2689impl CollectionState {
2690 pub fn new(
2693 primary: Option<GlobalId>,
2694 time_dependence: Option<TimeDependence>,
2695 ingestion_remap_collection_id: Option<GlobalId>,
2696 since: Antichain<Timestamp>,
2697 write_frontier: Antichain<Timestamp>,
2698 storage_dependencies: Vec<GlobalId>,
2699 metadata: CollectionMetadata,
2700 ) -> Self {
2701 let mut read_capabilities = MutableAntichain::new();
2702 read_capabilities.update_iter(since.iter().map(|time| (*time, 1)));
2703 Self {
2704 primary,
2705 time_dependence,
2706 ingestion_remap_collection_id,
2707 read_capabilities,
2708 implied_capability: since.clone(),
2709 read_policy: ReadPolicy::NoPolicy {
2710 initial_since: since,
2711 },
2712 storage_dependencies,
2713 write_frontier,
2714 collection_metadata: metadata,
2715 }
2716 }
2717
2718 pub fn is_dropped(&self) -> bool {
2720 self.read_capabilities.is_empty()
2721 }
2722}
2723
2724#[derive(Debug)]
2730struct BackgroundTask {
2731 config: Arc<Mutex<StorageConfiguration>>,
2732 cmds_tx: mpsc::UnboundedSender<BackgroundCmd>,
2733 cmds_rx: mpsc::UnboundedReceiver<BackgroundCmd>,
2734 holds_rx: mpsc::UnboundedReceiver<(GlobalId, ChangeBatch<Timestamp>)>,
2735 finalizable_shards: Arc<ShardIdSet>,
2736 collections: Arc<std::sync::Mutex<BTreeMap<GlobalId, CollectionState>>>,
2737 shard_by_id: BTreeMap<GlobalId, ShardId>,
2740 since_handles: BTreeMap<GlobalId, SinceHandleWrapper>,
2741 txns_handle: Option<WriteHandle<SourceData, (), Timestamp, StorageDiff>>,
2742 txns_shards: BTreeSet<GlobalId>,
2743}
2744
2745#[derive(Debug)]
2746enum BackgroundCmd {
2747 Register {
2748 id: GlobalId,
2749 is_in_txns: bool,
2750 write_handle: WriteHandle<SourceData, (), Timestamp, StorageDiff>,
2751 since_handle: SinceHandleWrapper,
2752 },
2753 DowngradeSince(Vec<(GlobalId, Antichain<Timestamp>)>),
2754 SnapshotStats(
2755 GlobalId,
2756 SnapshotStatsAsOf,
2757 oneshot::Sender<SnapshotStatsRes>,
2758 ),
2759}
2760
2761pub(crate) struct SnapshotStatsRes(BoxFuture<'static, Result<SnapshotStats, StorageError>>);
2763
2764impl Debug for SnapshotStatsRes {
2765 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2766 f.debug_struct("SnapshotStatsRes").finish_non_exhaustive()
2767 }
2768}
2769
2770impl BackgroundTask {
2771 async fn run(&mut self) {
2772 let mut upper_futures: FuturesUnordered<
2774 std::pin::Pin<
2775 Box<
2776 dyn Future<
2777 Output = (
2778 GlobalId,
2779 WriteHandle<SourceData, (), Timestamp, StorageDiff>,
2780 Antichain<Timestamp>,
2781 ),
2782 > + Send,
2783 >,
2784 >,
2785 > = FuturesUnordered::new();
2786
2787 let gen_upper_future =
2788 |id, mut handle: WriteHandle<_, _, _, _>, prev_upper: Antichain<Timestamp>| {
2789 let fut = async move {
2790 soft_assert_or_log!(
2791 !prev_upper.is_empty(),
2792 "cannot await progress when upper is already empty"
2793 );
2794 handle.wait_for_upper_past(&prev_upper).await;
2795 let new_upper = handle.shared_upper();
2796 (id, handle, new_upper)
2797 };
2798
2799 fut
2800 };
2801
2802 let mut txns_upper_future = match self.txns_handle.take() {
2803 Some(txns_handle) => {
2804 let upper = txns_handle.upper().clone();
2805 let txns_upper_future =
2806 gen_upper_future(GlobalId::Transient(1), txns_handle, upper);
2807 txns_upper_future.boxed()
2808 }
2809 None => async { std::future::pending().await }.boxed(),
2810 };
2811
2812 loop {
2813 tokio::select! {
2814 (id, handle, upper) = &mut txns_upper_future => {
2815 trace!("new upper from txns shard: {:?}", upper);
2816 let mut uppers = Vec::new();
2817 for id in self.txns_shards.iter() {
2818 uppers.push((*id, &upper));
2819 }
2820 self.update_write_frontiers(&uppers).await;
2821
2822 let fut = gen_upper_future(id, handle, upper);
2823 txns_upper_future = fut.boxed();
2824 }
2825 Some((id, handle, upper)) = upper_futures.next() => {
2826 if id.is_user() {
2827 trace!("new upper for collection {id}: {:?}", upper);
2828 }
2829 let current_shard = self.shard_by_id.get(&id);
2830 if let Some(shard_id) = current_shard {
2831 if shard_id == &handle.shard_id() {
2832 let uppers = &[(id, &upper)];
2835 self.update_write_frontiers(uppers).await;
2836 if !upper.is_empty() {
2837 let fut = gen_upper_future(id, handle, upper);
2838 upper_futures.push(fut.boxed());
2839 }
2840 } else {
2841 handle.expire().await;
2845 }
2846 }
2847 }
2848 cmd = self.cmds_rx.recv() => {
2849 let Some(cmd) = cmd else {
2850 break;
2852 };
2853
2854 let commands = iter::once(cmd).chain(
2858 iter::from_fn(|| self.cmds_rx.try_recv().ok())
2859 );
2860 let mut downgrades = BTreeMap::<_, Antichain<_>>::new();
2861 for cmd in commands {
2862 match cmd {
2863 BackgroundCmd::Register{
2864 id,
2865 is_in_txns,
2866 write_handle,
2867 since_handle
2868 } => {
2869 debug!("registering handles for {}", id);
2870 let previous = self.shard_by_id.insert(id, write_handle.shard_id());
2871 if previous.is_some() {
2872 panic!("already registered a WriteHandle for collection {id}");
2873 }
2874
2875 let previous = self.since_handles.insert(id, since_handle);
2876 if previous.is_some() {
2877 panic!("already registered a SinceHandle for collection {id}");
2878 }
2879
2880 if is_in_txns {
2881 self.txns_shards.insert(id);
2882 } else {
2883 let upper = write_handle.upper().clone();
2884 if !upper.is_empty() {
2885 let fut = gen_upper_future(id, write_handle, upper);
2886 upper_futures.push(fut.boxed());
2887 }
2888 }
2889 }
2890 BackgroundCmd::DowngradeSince(cmds) => {
2891 for (id, new) in cmds {
2892 downgrades.entry(id)
2893 .and_modify(|since| since.join_assign(&new))
2894 .or_insert(new);
2895 }
2896 }
2897 BackgroundCmd::SnapshotStats(id, as_of, tx) => {
2898 let res = match self.since_handles.get(&id) {
2904 Some(x) => {
2905 let fut: BoxFuture<
2906 'static,
2907 Result<SnapshotStats, StorageError>,
2908 > = match as_of {
2909 SnapshotStatsAsOf::Direct(as_of) => {
2910 x.snapshot_stats(id, Some(as_of))
2911 }
2912 SnapshotStatsAsOf::Txns(data_snapshot) => {
2913 x.snapshot_stats_from_txn(id, data_snapshot)
2914 }
2915 };
2916 SnapshotStatsRes(fut)
2917 }
2918 None => SnapshotStatsRes(Box::pin(futures::future::ready(Err(
2919 StorageError::IdentifierMissing(id),
2920 )))),
2921 };
2922 let _ = tx.send(res);
2924 }
2925 }
2926 }
2927
2928 if !downgrades.is_empty() {
2929 self.downgrade_sinces(downgrades).await;
2930 }
2931 }
2932 Some(holds_changes) = self.holds_rx.recv() => {
2933 let mut batched_changes = BTreeMap::new();
2934 batched_changes.insert(holds_changes.0, holds_changes.1);
2935
2936 while let Ok(mut holds_changes) = self.holds_rx.try_recv() {
2937 let entry = batched_changes.entry(holds_changes.0);
2938 entry
2939 .and_modify(|existing| existing.extend(holds_changes.1.drain()))
2940 .or_insert_with(|| holds_changes.1);
2941 }
2942
2943 let mut collections = self.collections.lock().expect("lock poisoned");
2944
2945 let user_changes = batched_changes
2946 .iter()
2947 .filter(|(id, _c)| id.is_user())
2948 .map(|(id, c)| {
2949 (id.clone(), c.clone())
2950 })
2951 .collect_vec();
2952
2953 if !user_changes.is_empty() {
2954 trace!(?user_changes, "applying holds changes from channel");
2955 }
2956
2957 StorageCollectionsImpl::update_read_capabilities_inner(
2958 &self.cmds_tx,
2959 &mut collections,
2960 &mut batched_changes,
2961 );
2962 }
2963 }
2964 }
2965
2966 warn!("BackgroundTask shutting down");
2967 }
2968
2969 #[instrument(level = "debug")]
2970 async fn update_write_frontiers(&self, updates: &[(GlobalId, &Antichain<Timestamp>)]) {
2971 let mut read_capability_changes = BTreeMap::default();
2972
2973 let mut self_collections = self.collections.lock().expect("lock poisoned");
2974
2975 for (id, new_upper) in updates.iter() {
2976 let collection = if let Some(c) = self_collections.get_mut(id) {
2977 c
2978 } else {
2979 trace!(
2980 "Reference to absent collection {id}, due to concurrent removal of that collection"
2981 );
2982 continue;
2983 };
2984
2985 if PartialOrder::less_than(&collection.write_frontier, *new_upper) {
2986 collection.write_frontier.clone_from(new_upper);
2987 }
2988
2989 let mut new_read_capability = collection
2990 .read_policy
2991 .frontier(collection.write_frontier.borrow());
2992
2993 if id.is_user() {
2994 trace!(
2995 %id,
2996 implied_capability = ?collection.implied_capability,
2997 policy = ?collection.read_policy,
2998 write_frontier = ?collection.write_frontier,
2999 ?new_read_capability,
3000 "update_write_frontiers");
3001 }
3002
3003 if PartialOrder::less_equal(&collection.implied_capability, &new_read_capability) {
3004 let mut update = ChangeBatch::new();
3005 update.extend(new_read_capability.iter().map(|time| (*time, 1)));
3006 std::mem::swap(&mut collection.implied_capability, &mut new_read_capability);
3007 update.extend(new_read_capability.iter().map(|time| (*time, -1)));
3008
3009 if !update.is_empty() {
3010 read_capability_changes.insert(*id, update);
3011 }
3012 }
3013 }
3014
3015 if !read_capability_changes.is_empty() {
3016 StorageCollectionsImpl::update_read_capabilities_inner(
3017 &self.cmds_tx,
3018 &mut self_collections,
3019 &mut read_capability_changes,
3020 );
3021 }
3022 }
3023
3024 async fn downgrade_sinces(&mut self, cmds: BTreeMap<GlobalId, Antichain<Timestamp>>) {
3025 let mut futures = Vec::with_capacity(cmds.len());
3027 for (id, new_since) in cmds {
3028 let Some(mut since_handle) = self.since_handles.remove(&id) else {
3031 trace!("downgrade_sinces: reference to absent collection {id}");
3033 continue;
3034 };
3035
3036 let fut = async move {
3037 if id.is_user() {
3038 trace!("downgrading since of {} to {:?}", id, new_since);
3039 }
3040
3041 let epoch = since_handle.opaque().clone();
3042 let result = if new_since.is_empty() {
3043 Some(
3047 since_handle
3048 .compare_and_downgrade_since(&epoch, (&epoch, &new_since))
3049 .await,
3050 )
3051 } else {
3052 since_handle
3053 .maybe_compare_and_downgrade_since(&epoch, (&epoch, &new_since))
3054 .await
3055 };
3056 (id, since_handle, result)
3057 };
3058 futures.push(fut);
3059 }
3060
3061 for (id, since_handle, result) in futures::future::join_all(futures).await {
3062 let new_since = match result {
3063 Some(Ok(since)) => Some(since),
3064 Some(Err(other_epoch)) => mz_ore::halt!(
3065 "fenced by envd @ {other_epoch:?}. ours = {:?}",
3066 since_handle.opaque(),
3067 ),
3068 None => None,
3069 };
3070
3071 self.since_handles.insert(id, since_handle);
3072
3073 if new_since.is_some_and(|s| s.is_empty()) {
3074 info!(%id, "removing persist handles because the since advanced to []!");
3075
3076 let _since_handle = self.since_handles.remove(&id).expect("known to exist");
3077 let Some(dropped_shard_id) = self.shard_by_id.remove(&id) else {
3078 panic!("missing GlobalId -> ShardId mapping for id {id}");
3079 };
3080
3081 self.txns_shards.remove(&id);
3086
3087 if self
3088 .config
3089 .lock()
3090 .expect("lock poisoned")
3091 .parameters
3092 .finalize_shards
3093 {
3094 info!(
3095 %id, %dropped_shard_id,
3096 "enqueuing shard finalization due to dropped collection and dropped \
3097 persist handle",
3098 );
3099 self.finalizable_shards.lock().insert(dropped_shard_id);
3100 } else {
3101 info!(
3102 "not triggering shard finalization due to dropped storage object \
3103 because enable_storage_shard_finalization parameter is false"
3104 );
3105 }
3106 }
3107 }
3108 }
3109}
3110
3111struct FinalizeShardsTaskConfig {
3112 envd_epoch: NonZeroI64,
3113 config: Arc<Mutex<StorageConfiguration>>,
3114 metrics: StorageCollectionsMetrics,
3115 finalizable_shards: Arc<ShardIdSet>,
3116 finalized_shards: Arc<ShardIdSet>,
3117 persist_location: PersistLocation,
3118 persist: Arc<PersistClientCache>,
3119 read_only: bool,
3120}
3121
3122async fn finalize_shards_task(
3123 FinalizeShardsTaskConfig {
3124 envd_epoch,
3125 config,
3126 metrics,
3127 finalizable_shards,
3128 finalized_shards,
3129 persist_location,
3130 persist,
3131 read_only,
3132 }: FinalizeShardsTaskConfig,
3133) {
3134 if read_only {
3135 info!("disabling shard finalization in read only mode");
3136 return;
3137 }
3138
3139 let mut interval = tokio::time::interval(Duration::from_secs(5));
3140 interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
3141 loop {
3142 interval.tick().await;
3143
3144 if !config
3145 .lock()
3146 .expect("lock poisoned")
3147 .parameters
3148 .finalize_shards
3149 {
3150 debug!(
3151 "not triggering shard finalization due to dropped storage object because enable_storage_shard_finalization parameter is false"
3152 );
3153 continue;
3154 }
3155
3156 let current_finalizable_shards = {
3157 finalizable_shards.lock().iter().cloned().collect_vec()
3160 };
3161
3162 if current_finalizable_shards.is_empty() {
3163 debug!("no shards to finalize");
3164 continue;
3165 }
3166
3167 debug!(?current_finalizable_shards, "attempting to finalize shards");
3168
3169 let persist_client = persist.open(persist_location.clone()).await.unwrap();
3171
3172 let metrics = &metrics;
3173 let finalizable_shards = &finalizable_shards;
3174 let finalized_shards = &finalized_shards;
3175 let persist_client = &persist_client;
3176 let diagnostics = &Diagnostics::from_purpose("finalizing shards");
3177
3178 let force_downgrade_since = STORAGE_DOWNGRADE_SINCE_DURING_FINALIZATION
3179 .get(config.lock().expect("lock poisoned").config_set());
3180
3181 let epoch = &PersistEpoch::from(envd_epoch);
3182
3183 futures::stream::iter(current_finalizable_shards.clone())
3184 .map(|shard_id| async move {
3185 let persist_client = persist_client.clone();
3186 let diagnostics = diagnostics.clone();
3187 let epoch = epoch.clone();
3188
3189 metrics.finalization_started.inc();
3190
3191 let is_finalized = persist_client
3192 .is_finalized::<SourceData, (), Timestamp, StorageDiff>(shard_id, diagnostics)
3193 .await
3194 .expect("invalid persist usage");
3195
3196 if is_finalized {
3197 debug!(%shard_id, "shard is already finalized!");
3198 Some(shard_id)
3199 } else {
3200 debug!(%shard_id, "finalizing shard");
3201 let finalize = || async move {
3202 let diagnostics = Diagnostics::from_purpose("finalizing shards");
3204
3205 let mut write_handle: WriteHandle<SourceData, (), Timestamp, StorageDiff> =
3208 persist_client
3209 .open_writer(
3210 shard_id,
3211 Arc::new(RelationDesc::empty()),
3212 Arc::new(UnitSchema),
3213 diagnostics,
3214 )
3215 .await
3216 .expect("invalid persist usage");
3217 write_handle.advance_upper(&Antichain::new()).await;
3218 write_handle.expire().await;
3219
3220 if force_downgrade_since {
3221 let our_opaque = Opaque::encode(&epoch);
3222 let mut since_handle: SinceHandle<
3223 SourceData,
3224 (),
3225 Timestamp,
3226 StorageDiff,
3227 > = persist_client
3228 .open_critical_since(
3229 shard_id,
3230 PersistClient::CONTROLLER_CRITICAL_SINCE,
3231 our_opaque.clone(),
3232 Diagnostics::from_purpose("finalizing shards"),
3233 )
3234 .await
3235 .expect("invalid persist usage");
3236 let handle_opaque = since_handle.opaque().clone();
3237 let opaque = if our_opaque.codec_name() == handle_opaque.codec_name()
3238 && epoch.0 > handle_opaque.decode::<PersistEpoch>().0
3239 {
3240 handle_opaque
3243 } else {
3244 our_opaque
3250 };
3251 let new_since = Antichain::new();
3252 let downgrade = since_handle
3253 .compare_and_downgrade_since(&opaque, (&opaque, &new_since))
3254 .await;
3255 if let Err(e) = downgrade {
3256 warn!("tried to finalize a shard with an advancing epoch: {e:?}");
3257 return Ok(());
3258 }
3259 }
3262
3263 persist_client
3264 .finalize_shard::<SourceData, (), Timestamp, StorageDiff>(
3265 shard_id,
3266 Diagnostics::from_purpose("finalizing shards"),
3267 )
3268 .await
3269 };
3270
3271 match finalize().await {
3272 Err(e) => {
3273 warn!("error during finalization of shard {shard_id}: {e:?}");
3276 None
3277 }
3278 Ok(()) => {
3279 debug!(%shard_id, "finalize success!");
3280 Some(shard_id)
3281 }
3282 }
3283 }
3284 })
3285 .buffer_unordered(10)
3290 .for_each(|shard_id| async move {
3294 match shard_id {
3295 None => metrics.finalization_failed.inc(),
3296 Some(shard_id) => {
3297 {
3304 let mut finalizable_shards = finalizable_shards.lock();
3305 let mut finalized_shards = finalized_shards.lock();
3306 finalizable_shards.remove(&shard_id);
3307 finalized_shards.insert(shard_id);
3308 }
3309
3310 metrics.finalization_succeeded.inc();
3311 }
3312 }
3313 })
3314 .await;
3315
3316 debug!("done finalizing shards");
3317 }
3318}
3319
3320#[derive(Debug)]
3323struct DependencyViolation {
3324 id: GlobalId,
3325 dependency: GlobalId,
3326 data_shard_since: Antichain<Timestamp>,
3327 write_frontier: Antichain<Timestamp>,
3328 dependency_since: Antichain<Timestamp>,
3329 ingestion_export: Option<CollectionMetadata>,
3331}
3332
3333pub async fn shard_has_no_updates(
3336 persist_client: &PersistClient,
3337 id: GlobalId,
3338 metadata: &CollectionMetadata,
3339) -> bool {
3340 let read_handle = persist_client
3341 .open_leased_reader::<SourceData, (), Timestamp, StorageDiff>(
3342 metadata.data_shard,
3343 Arc::new(metadata.relation_desc.clone()),
3344 Arc::new(UnitSchema),
3345 Diagnostics {
3346 shard_name: id.to_string(),
3347 handle_purpose: format!("checking {id} for updates"),
3348 },
3349 false,
3350 )
3351 .await
3352 .expect("invalid persist usage");
3353 let stats = read_handle
3354 .snapshot_stats(None)
3355 .await
3356 .expect("stats without an as_of cannot be behind the since");
3357 read_handle.expire().await;
3358 stats.num_updates == 0
3359}
3360
3361#[derive(Debug)]
3362pub(crate) enum SnapshotStatsAsOf {
3363 Direct(Antichain<Timestamp>),
3366 Txns(DataSnapshot<Timestamp>),
3369}
3370
3371#[cfg(test)]
3372mod tests {
3373 use std::str::FromStr;
3374 use std::sync::Arc;
3375
3376 use mz_build_info::DUMMY_BUILD_INFO;
3377 use mz_dyncfg::ConfigSet;
3378 use mz_ore::assert_err;
3379 use mz_ore::metrics::{MetricsRegistry, UIntGauge};
3380 use mz_ore::now::SYSTEM_TIME;
3381 use mz_ore::url::SensitiveUrl;
3382 use mz_persist_client::cache::PersistClientCache;
3383 use mz_persist_client::cfg::PersistConfig;
3384 use mz_persist_client::rpc::PubSubClientConnection;
3385 use mz_persist_client::{Diagnostics, PersistClient, PersistLocation, ShardId};
3386 use mz_persist_types::codec_impls::UnitSchema;
3387 use mz_repr::{RelationDesc, Row};
3388 use mz_secrets::InMemorySecretsController;
3389
3390 use super::*;
3391
3392 #[mz_ore::test]
3393 fn test_partition_finalizable_shards() {
3394 let active_shard = ShardId::new();
3395 let dropped_shard = ShardId::new();
3396 let collection_metadata = BTreeMap::from([
3397 (GlobalId::User(1), active_shard),
3398 (GlobalId::User(2), active_shard),
3399 (GlobalId::User(3), dropped_shard),
3400 ]);
3401 let active_collection_ids = BTreeSet::from([GlobalId::User(1), GlobalId::User(2)]);
3402 let unfinalized_shards = BTreeSet::from([active_shard, dropped_shard]);
3403
3404 let (referenced_shards, finalizable_shards) = partition_finalizable_shards(
3405 collection_metadata,
3406 &active_collection_ids,
3407 unfinalized_shards,
3408 );
3409
3410 assert_eq!(referenced_shards, BTreeSet::from([active_shard]));
3411 assert_eq!(finalizable_shards, BTreeSet::from([dropped_shard]));
3412 }
3413
3414 #[mz_ore::test(tokio::test)]
3415 #[cfg_attr(miri, ignore)] async fn test_snapshot_stats(&self) {
3417 let persist_location = PersistLocation {
3418 blob_uri: SensitiveUrl::from_str("mem://").expect("invalid URL"),
3419 consensus_uri: SensitiveUrl::from_str("mem://").expect("invalid URL"),
3420 };
3421 let persist_client = PersistClientCache::new(
3422 PersistConfig::new_default_configs(&DUMMY_BUILD_INFO, SYSTEM_TIME.clone()),
3423 &MetricsRegistry::new(),
3424 |_, _| PubSubClientConnection::noop(),
3425 );
3426 let persist_client = Arc::new(persist_client);
3427
3428 let (cmds_tx, mut background_task) =
3429 BackgroundTask::new_for_test(persist_location.clone(), Arc::clone(&persist_client));
3430 let background_task =
3431 mz_ore::task::spawn(|| "storage_collections::background_task", async move {
3432 background_task.run().await
3433 });
3434
3435 let persist = persist_client.open(persist_location).await.unwrap();
3436
3437 let shard_id = ShardId::new();
3438 let since_handle = persist
3439 .open_critical_since(
3440 shard_id,
3441 PersistClient::CONTROLLER_CRITICAL_SINCE,
3442 Opaque::encode(&PersistEpoch::default()),
3443 Diagnostics::for_tests(),
3444 )
3445 .await
3446 .unwrap();
3447 let write_handle = persist
3448 .open_writer::<SourceData, (), mz_repr::Timestamp, StorageDiff>(
3449 shard_id,
3450 Arc::new(RelationDesc::empty()),
3451 Arc::new(UnitSchema),
3452 Diagnostics::for_tests(),
3453 )
3454 .await
3455 .unwrap();
3456
3457 cmds_tx
3458 .send(BackgroundCmd::Register {
3459 id: GlobalId::User(1),
3460 is_in_txns: false,
3461 since_handle: SinceHandleWrapper::Critical(since_handle),
3462 write_handle,
3463 })
3464 .unwrap();
3465
3466 let mut write_handle = persist
3467 .open_writer::<SourceData, (), mz_repr::Timestamp, StorageDiff>(
3468 shard_id,
3469 Arc::new(RelationDesc::empty()),
3470 Arc::new(UnitSchema),
3471 Diagnostics::for_tests(),
3472 )
3473 .await
3474 .unwrap();
3475
3476 let stats =
3478 snapshot_stats(&cmds_tx, GlobalId::User(2), Antichain::from_elem(0.into())).await;
3479 assert_err!(stats);
3480
3481 let stats_fut = snapshot_stats(&cmds_tx, GlobalId::User(1), Antichain::from_elem(1.into()));
3483 assert_none!(stats_fut.now_or_never());
3484
3485 let stats_ts1_fut =
3487 snapshot_stats(&cmds_tx, GlobalId::User(1), Antichain::from_elem(1.into()));
3488
3489 let data = (
3491 (SourceData(Ok(Row::default())), ()),
3492 mz_repr::Timestamp::from(0),
3493 1i64,
3494 );
3495 let () = write_handle
3496 .compare_and_append(
3497 &[data],
3498 Antichain::from_elem(0.into()),
3499 Antichain::from_elem(1.into()),
3500 )
3501 .await
3502 .unwrap()
3503 .unwrap();
3504
3505 let stats = snapshot_stats(&cmds_tx, GlobalId::User(1), Antichain::from_elem(0.into()))
3507 .await
3508 .unwrap();
3509 assert_eq!(stats.num_updates, 1);
3510
3511 let data = (
3513 (SourceData(Ok(Row::default())), ()),
3514 mz_repr::Timestamp::from(1),
3515 1i64,
3516 );
3517 let () = write_handle
3518 .compare_and_append(
3519 &[data],
3520 Antichain::from_elem(1.into()),
3521 Antichain::from_elem(2.into()),
3522 )
3523 .await
3524 .unwrap()
3525 .unwrap();
3526
3527 let stats = stats_ts1_fut.await.unwrap();
3528 assert_eq!(stats.num_updates, 2);
3529
3530 drop(background_task);
3532 }
3533
3534 async fn snapshot_stats(
3535 cmds_tx: &mpsc::UnboundedSender<BackgroundCmd>,
3536 id: GlobalId,
3537 as_of: Antichain<Timestamp>,
3538 ) -> Result<SnapshotStats, StorageError> {
3539 let (tx, rx) = oneshot::channel();
3540 cmds_tx
3541 .send(BackgroundCmd::SnapshotStats(
3542 id,
3543 SnapshotStatsAsOf::Direct(as_of),
3544 tx,
3545 ))
3546 .unwrap();
3547 let res = rx.await.expect("BackgroundTask should be live").0;
3548
3549 res.await
3550 }
3551
3552 impl BackgroundTask {
3553 fn new_for_test(
3554 _persist_location: PersistLocation,
3555 _persist_client: Arc<PersistClientCache>,
3556 ) -> (mpsc::UnboundedSender<BackgroundCmd>, Self) {
3557 let (cmds_tx, cmds_rx) = mpsc::unbounded_channel();
3558 let (_holds_tx, holds_rx) = mpsc::unbounded_channel();
3559 let connection_context =
3560 ConnectionContext::for_tests(Arc::new(InMemorySecretsController::new()));
3561
3562 let task = Self {
3563 config: Arc::new(Mutex::new(StorageConfiguration::new(
3564 connection_context,
3565 ConfigSet::default(),
3566 ))),
3567 cmds_tx: cmds_tx.clone(),
3568 cmds_rx,
3569 holds_rx,
3570 finalizable_shards: Arc::new(ShardIdSet::new(
3571 UIntGauge::new("finalizable_shards", "dummy gauge for tests").unwrap(),
3572 )),
3573 collections: Arc::new(Mutex::new(BTreeMap::new())),
3574 shard_by_id: BTreeMap::new(),
3575 since_handles: BTreeMap::new(),
3576 txns_handle: None,
3577 txns_shards: BTreeSet::new(),
3578 };
3579
3580 (cmds_tx, task)
3581 }
3582 }
3583}