1use std::any::Any;
13use std::collections::btree_map;
14use std::collections::{BTreeMap, BTreeSet};
15use std::fmt::Debug;
16use std::str::FromStr;
17use std::sync::{Arc, Mutex};
18use std::time::Duration;
19
20use crate::collection_mgmt::{
21 AppendOnlyIntrospectionConfig, CollectionManagerKind, DifferentialIntrospectionConfig,
22};
23use crate::instance::{Instance, ReplicaConfig};
24use async_trait::async_trait;
25use chrono::{DateTime, DurationRound, TimeDelta, Utc};
26use derivative::Derivative;
27use differential_dataflow::lattice::Lattice;
28use futures::FutureExt;
29use futures::StreamExt;
30use itertools::Itertools;
31use mz_build_info::BuildInfo;
32use mz_cluster_client::client::ClusterReplicaLocation;
33use mz_cluster_client::metrics::{ControllerMetrics, WallclockLagMetrics};
34use mz_cluster_client::{ReplicaId, WallclockLagFn};
35use mz_controller_types::dyncfgs::{
36 ENABLE_0DT_DEPLOYMENT_SOURCES, WALLCLOCK_LAG_RECORDING_INTERVAL,
37};
38use mz_dyncfg::ConfigUpdates;
39use mz_ore::collections::CollectionExt;
40use mz_ore::metrics::MetricsRegistry;
41use mz_ore::now::NowFn;
42use mz_ore::task::AbortOnDropHandle;
43use mz_ore::{assert_none, halt, instrument, soft_panic_or_log};
44use mz_persist_client::batch::ProtoBatch;
45use mz_persist_client::cache::PersistClientCache;
46use mz_persist_client::cfg::USE_CRITICAL_SINCE_SNAPSHOT;
47use mz_persist_client::critical::Opaque;
48use mz_persist_client::read::ReadHandle;
49use mz_persist_client::schema::CaESchema;
50use mz_persist_client::write::WriteHandle;
51use mz_persist_client::{Diagnostics, PersistClient, PersistLocation, ShardId};
52use mz_persist_types::codec_impls::UnitSchema;
53use mz_repr::adt::timestamp::CheckedTimestamp;
54use mz_repr::{Datum, Diff, GlobalId, RelationDesc, RelationVersion, Row, Timestamp};
55use mz_storage_client::client::{
56 AppendOnlyUpdate, RunIngestionCommand, RunOneshotIngestion, RunSinkCommand, Status,
57 StatusUpdate, StorageCommand, StorageResponse, TableData,
58};
59use mz_storage_client::controller::{
60 BoxFuture, CollectionDescription, DataSource, ExportDescription, ExportState,
61 IntrospectionType, MonotonicAppender, PersistEpoch, Response, StorageController,
62 StorageMetadata, StorageTxn, StorageWriteOp, TableRegistration, WallclockLag,
63 WallclockLagHistogramPeriod,
64};
65use mz_storage_client::healthcheck::{
66 MZ_AWS_PRIVATELINK_CONNECTION_STATUS_HISTORY_DESC, MZ_SINK_STATUS_HISTORY_DESC,
67 MZ_SOURCE_STATUS_HISTORY_DESC, REPLICA_STATUS_HISTORY_DESC,
68};
69use mz_storage_client::metrics::StorageControllerMetrics;
70use mz_storage_client::statistics::{
71 ControllerSinkStatistics, ControllerSourceStatistics, WebhookStatistics,
72};
73use mz_storage_client::storage_collections::StorageCollections;
74use mz_storage_types::configuration::StorageConfiguration;
75use mz_storage_types::connections::ConnectionContext;
76use mz_storage_types::connections::inline::InlinedConnection;
77use mz_storage_types::controller::{AlterError, CollectionMetadata, StorageError, TxnsCodecRow};
78use mz_storage_types::errors::CollectionMissing;
79use mz_storage_types::instances::StorageInstanceId;
80use mz_storage_types::oneshot_sources::{OneshotIngestionRequest, OneshotResultCallback};
81use mz_storage_types::parameters::StorageParameters;
82use mz_storage_types::read_holds::ReadHold;
83use mz_storage_types::read_policy::ReadPolicy;
84use mz_storage_types::sinks::{StorageSinkConnection, StorageSinkDesc};
85use mz_storage_types::sources::{
86 GenericSourceConnection, IngestionDescription, SourceConnection, SourceData, SourceDesc,
87 SourceExport, SourceExportDataConfig,
88};
89use mz_storage_types::{AlterCompatible, StorageDiff, dyncfgs};
90use mz_txn_wal::metrics::Metrics as TxnMetrics;
91use mz_txn_wal::txn_read::TxnsRead;
92use mz_txn_wal::txns::TxnsHandle;
93use timely::order::PartialOrder;
94use timely::progress::frontier::MutableAntichain;
95use timely::progress::{Antichain, ChangeBatch};
96use tokio::sync::watch::{Sender, channel};
97use tokio::sync::{mpsc, oneshot};
98use tokio::time::MissedTickBehavior;
99use tokio::time::error::Elapsed;
100use tracing::{debug, info, warn};
101
102mod collection_mgmt;
103mod history;
104mod instance;
105mod persist_handles;
106mod rtr;
107mod statistics;
108
109#[derive(Derivative)]
110#[derivative(Debug)]
111struct PendingOneshotIngestion {
112 #[derivative(Debug = "ignore")]
114 result_tx: OneshotResultCallback<ProtoBatch>,
115 cluster_id: StorageInstanceId,
117}
118
119impl PendingOneshotIngestion {
120 pub(crate) fn cancel(self) {
124 (self.result_tx)(vec![Err("canceled".to_string())])
125 }
126}
127
128#[derive(Derivative)]
130#[derivative(Debug)]
131pub struct Controller {
132 build_info: &'static BuildInfo,
134 now: NowFn,
136
137 read_only: bool,
143
144 pub(crate) collections: BTreeMap<GlobalId, CollectionState>,
149
150 dropped_objects: BTreeMap<GlobalId, BTreeSet<ReplicaId>>,
159
160 pub(crate) persist_table_worker: persist_handles::PersistTableWriteWorker,
162 txns_read: TxnsRead<Timestamp>,
164 txns_metrics: Arc<TxnMetrics>,
165 stashed_responses: Vec<(Option<ReplicaId>, StorageResponse)>,
166 #[derivative(Debug = "ignore")]
168 pending_table_handle_drops_tx: mpsc::UnboundedSender<GlobalId>,
169 #[derivative(Debug = "ignore")]
171 pending_table_handle_drops_rx: mpsc::UnboundedReceiver<GlobalId>,
172 #[derivative(Debug = "ignore")]
174 pending_oneshot_ingestions: BTreeMap<uuid::Uuid, PendingOneshotIngestion>,
175
176 pub(crate) collection_manager: collection_mgmt::CollectionManager,
178
179 pub(crate) introspection_ids: BTreeMap<IntrospectionType, GlobalId>,
181 introspection_tokens: Arc<Mutex<BTreeMap<GlobalId, Box<dyn Any + Send + Sync>>>>,
186
187 source_statistics: Arc<Mutex<statistics::SourceStatistics>>,
192 sink_statistics: Arc<Mutex<BTreeMap<(GlobalId, Option<ReplicaId>), ControllerSinkStatistics>>>,
195 statistics_interval_sender: Sender<Duration>,
197
198 instances: BTreeMap<StorageInstanceId, Instance>,
200 replica_dyncfg_overrides: BTreeMap<ReplicaId, ConfigUpdates>,
205 initialized: bool,
207 config: StorageConfiguration,
209 persist_location: PersistLocation,
211 persist: Arc<PersistClientCache>,
213 metrics: StorageControllerMetrics,
215 recorded_frontiers: BTreeMap<GlobalId, (Antichain<Timestamp>, Antichain<Timestamp>)>,
218 recorded_replica_frontiers: BTreeMap<(GlobalId, ReplicaId), Antichain<Timestamp>>,
221
222 #[derivative(Debug = "ignore")]
224 wallclock_lag: WallclockLagFn<Timestamp>,
225 wallclock_lag_last_recorded: DateTime<Utc>,
227
228 storage_collections: Arc<dyn StorageCollections + Send + Sync>,
230 migrated_storage_collections: BTreeSet<GlobalId>,
232
233 maintenance_ticker: tokio::time::Interval,
235 maintenance_scheduled: bool,
237
238 instance_response_tx: mpsc::UnboundedSender<(Option<ReplicaId>, StorageResponse)>,
240 instance_response_rx: mpsc::UnboundedReceiver<(Option<ReplicaId>, StorageResponse)>,
242
243 persist_warm_task: Option<AbortOnDropHandle<Box<dyn Debug + Send>>>,
245}
246
247enum WriteHandleOrUpper {
256 Handle(WriteHandle<SourceData, (), Timestamp, StorageDiff>),
257 Upper(Antichain<Timestamp>),
258}
259
260impl WriteHandleOrUpper {
261 fn upper(&self) -> Antichain<Timestamp> {
262 match self {
263 Self::Handle(handle) => handle.upper().clone(),
264 Self::Upper(upper) => upper.clone(),
265 }
266 }
267
268 fn expect_handle(self, context: &str) -> WriteHandle<SourceData, (), Timestamp, StorageDiff> {
271 match self {
272 Self::Handle(handle) => handle,
273 Self::Upper(_) => panic!("write handle required: {context}"),
274 }
275 }
276}
277
278fn warm_persist_state_in_background(
279 client: PersistClient,
280 shard_ids: impl Iterator<Item = ShardId> + Send + 'static,
281) -> mz_ore::task::JoinHandle<Box<dyn Debug + Send>> {
282 const MAX_CONCURRENT_WARMS: usize = 16;
284 let logic = async move {
285 let fetchers: Vec<_> = tokio_stream::iter(shard_ids)
286 .map(|shard_id| {
287 let client = client.clone();
288 async move {
289 client
290 .create_batch_fetcher::<SourceData, (), mz_repr::Timestamp, StorageDiff>(
291 shard_id,
292 Arc::new(RelationDesc::empty()),
293 Arc::new(UnitSchema),
294 true,
295 Diagnostics::from_purpose("warm persist load state"),
296 )
297 .await
298 }
299 })
300 .buffer_unordered(MAX_CONCURRENT_WARMS)
301 .collect()
302 .await;
303 let fetchers: Box<dyn Debug + Send> = Box::new(fetchers);
304 fetchers
305 };
306 mz_ore::task::spawn(|| "warm_persist_load_state", logic)
307}
308
309#[async_trait(?Send)]
310impl StorageController for Controller {
311 fn initialization_complete(&mut self) {
312 self.reconcile_dangling_statistics();
313 self.initialized = true;
314
315 for instance in self.instances.values_mut() {
316 instance.send(StorageCommand::InitializationComplete);
317 }
318 }
319
320 fn update_parameters(&mut self, config_params: StorageParameters) {
321 self.storage_collections
322 .update_parameters(config_params.clone());
323
324 self.persist.cfg().apply_from(&config_params.dyncfg_updates);
327
328 for instance in self.instances.values_mut() {
329 let params = Box::new(config_params.clone());
330 instance.send(StorageCommand::UpdateConfiguration(params));
331 }
332 self.config.update(config_params);
333 self.statistics_interval_sender
334 .send_replace(self.config.parameters.statistics_interval);
335 self.collection_manager.update_user_batch_duration(
336 self.config
337 .parameters
338 .user_storage_managed_collections_batch_duration,
339 );
340 }
341
342 fn update_replica_dyncfg_overrides(
343 &mut self,
344 mut overrides: BTreeMap<StorageInstanceId, BTreeMap<ReplicaId, ConfigUpdates>>,
345 ) {
346 self.replica_dyncfg_overrides = overrides
347 .values()
348 .flat_map(|replicas| replicas.iter())
349 .map(|(replica_id, updates)| (*replica_id, updates.clone()))
350 .collect();
351 for (id, instance) in self.instances.iter_mut() {
352 let instance_overrides = overrides.remove(id).unwrap_or_default();
353 instance.update_replica_dyncfg_overrides(instance_overrides);
354 }
355 }
356
357 fn config(&self) -> &StorageConfiguration {
359 &self.config
360 }
361
362 fn collection_metadata(&self, id: GlobalId) -> Result<CollectionMetadata, CollectionMissing> {
363 self.storage_collections.collection_metadata(id)
364 }
365
366 fn collection_hydrated(&self, collection_id: GlobalId) -> Result<bool, StorageError> {
367 let collection = self.collection(collection_id)?;
368
369 let instance_id = match &collection.data_source {
370 DataSource::Ingestion(ingestion_description) => ingestion_description.instance_id,
371 DataSource::IngestionExport { ingestion_id, .. } => {
372 let ingestion_state = self.collections.get(ingestion_id).expect("known to exist");
373
374 let instance_id = match &ingestion_state.data_source {
375 DataSource::Ingestion(ingestion_desc) => ingestion_desc.instance_id,
376 _ => unreachable!("SourceExport must only refer to primary source"),
377 };
378
379 instance_id
380 }
381 _ => return Ok(true),
382 };
383
384 let instance = self.instances.get(&instance_id).ok_or_else(|| {
385 StorageError::IngestionInstanceMissing {
386 storage_instance_id: instance_id,
387 ingestion_id: collection_id,
388 }
389 })?;
390
391 if instance.replica_ids().next().is_none() {
392 return Ok(true);
395 }
396
397 match &collection.extra_state {
398 CollectionStateExtra::Ingestion(ingestion_state) => {
399 Ok(ingestion_state.hydrated_on.len() >= 1)
401 }
402 CollectionStateExtra::Export(_) => {
403 Ok(true)
408 }
409 CollectionStateExtra::None => {
410 Ok(true)
414 }
415 }
416 }
417
418 #[mz_ore::instrument(level = "debug")]
419 fn collections_hydrated_on_replicas(
420 &self,
421 target_replica_ids: Option<Vec<ReplicaId>>,
422 target_cluster_id: &StorageInstanceId,
423 exclude_collections: &BTreeSet<GlobalId>,
424 ) -> Result<bool, StorageError> {
425 if target_replica_ids.as_ref().is_some_and(|v| v.is_empty()) {
428 return Ok(true);
429 }
430
431 let target_replicas: Option<BTreeSet<ReplicaId>> =
434 target_replica_ids.map(|ids| ids.into_iter().collect());
435
436 let instance = self.instances.get(target_cluster_id);
437
438 let mut all_hydrated = true;
439 for (collection_id, collection_state) in self.collections.iter() {
440 if collection_id.is_transient() || exclude_collections.contains(collection_id) {
441 continue;
442 }
443 let hydrated = match &collection_state.extra_state {
444 CollectionStateExtra::Ingestion(state) => {
445 if &state.instance_id != target_cluster_id {
446 continue;
447 }
448 match &target_replicas {
449 Some(target_replicas) => {
450 let scheduled_on = instance
458 .map(|i| i.get_active_replicas_for_object(collection_id))
459 .unwrap_or_default();
460 if scheduled_on.is_disjoint(target_replicas) {
461 true
462 } else {
463 !state.hydrated_on.is_disjoint(target_replicas)
464 }
465 }
466 None => {
467 state.hydrated_on.len() >= 1
470 }
471 }
472 }
473 CollectionStateExtra::Export(_) => {
474 true
479 }
480 CollectionStateExtra::None => {
481 true
485 }
486 };
487 if !hydrated {
488 tracing::info!(%collection_id, "collection is not hydrated on any replica");
489 all_hydrated = false;
490 }
493 }
494 Ok(all_hydrated)
495 }
496
497 fn collection_frontiers(
498 &self,
499 id: GlobalId,
500 ) -> Result<(Antichain<Timestamp>, Antichain<Timestamp>), CollectionMissing> {
501 let frontiers = self.storage_collections.collection_frontiers(id)?;
502 Ok((frontiers.implied_capability, frontiers.write_frontier))
503 }
504
505 fn collections_frontiers(
506 &self,
507 mut ids: Vec<GlobalId>,
508 ) -> Result<Vec<(GlobalId, Antichain<Timestamp>, Antichain<Timestamp>)>, CollectionMissing>
509 {
510 let mut result = vec![];
511 ids.retain(|&id| match self.export(id) {
516 Ok(export) => {
517 result.push((
518 id,
519 export.input_hold().since().clone(),
520 export.write_frontier.clone(),
521 ));
522 false
523 }
524 Err(_) => true,
525 });
526 result.extend(
527 self.storage_collections
528 .collections_frontiers(ids)?
529 .into_iter()
530 .map(|frontiers| {
531 (
532 frontiers.id,
533 frontiers.implied_capability,
534 frontiers.write_frontier,
535 )
536 }),
537 );
538
539 Ok(result)
540 }
541
542 fn active_collection_metadatas(&self) -> Vec<(GlobalId, CollectionMetadata)> {
543 self.storage_collections.active_collection_metadatas()
544 }
545
546 fn active_ingestion_exports(
547 &self,
548 instance_id: StorageInstanceId,
549 ) -> Box<dyn Iterator<Item = &GlobalId> + '_> {
550 let active_storage_collections: BTreeMap<_, _> = self
551 .storage_collections
552 .active_collection_frontiers()
553 .into_iter()
554 .map(|c| (c.id, c))
555 .collect();
556
557 let active_exports = self.instances[&instance_id]
558 .active_ingestion_exports()
559 .filter(move |id| {
560 let frontiers = active_storage_collections.get(id);
561 match frontiers {
562 Some(frontiers) => !frontiers.write_frontier.is_empty(),
563 None => {
564 false
566 }
567 }
568 });
569
570 Box::new(active_exports)
571 }
572
573 fn check_exists(&self, id: GlobalId) -> Result<(), StorageError> {
574 self.storage_collections.check_exists(id)
575 }
576
577 fn create_instance(&mut self, id: StorageInstanceId, workload_class: Option<String>) {
578 let metrics = self.metrics.for_instance(id);
579 let mut instance = Instance::new(
580 workload_class,
581 metrics,
582 self.now.clone(),
583 self.instance_response_tx.clone(),
584 );
585 if self.initialized {
586 instance.send(StorageCommand::InitializationComplete);
587 }
588 if !self.read_only {
589 instance.send(StorageCommand::AllowWrites);
590 }
591
592 let params = Box::new(self.config.parameters.clone());
593 instance.send(StorageCommand::UpdateConfiguration(params));
594
595 let old_instance = self.instances.insert(id, instance);
596 assert_none!(old_instance, "storage instance {id} already exists");
597 }
598
599 fn drop_instance(&mut self, id: StorageInstanceId) {
600 let instance = self.instances.remove(&id);
601 assert!(instance.is_some(), "storage instance {id} does not exist");
602 }
603
604 fn update_instance_workload_class(
605 &mut self,
606 id: StorageInstanceId,
607 workload_class: Option<String>,
608 ) {
609 let instance = self
610 .instances
611 .get_mut(&id)
612 .unwrap_or_else(|| panic!("instance {id} does not exist"));
613
614 instance.workload_class = workload_class;
615 }
616
617 fn connect_replica(
618 &mut self,
619 instance_id: StorageInstanceId,
620 replica_id: ReplicaId,
621 location: ClusterReplicaLocation,
622 ) {
623 let instance = self
624 .instances
625 .get_mut(&instance_id)
626 .unwrap_or_else(|| panic!("instance {instance_id} does not exist"));
627
628 let config = ReplicaConfig {
629 build_info: self.build_info,
630 location,
631 grpc_client: self.config.parameters.grpc_client.clone(),
632 };
633 let dyncfg_override = self
634 .replica_dyncfg_overrides
635 .get(&replica_id)
636 .cloned()
637 .unwrap_or_default();
638 instance.add_replica(replica_id, config, dyncfg_override);
639 }
640
641 fn drop_replica(&mut self, instance_id: StorageInstanceId, replica_id: ReplicaId) {
642 self.replica_dyncfg_overrides.remove(&replica_id);
646
647 let instance = self
648 .instances
649 .get_mut(&instance_id)
650 .unwrap_or_else(|| panic!("instance {instance_id} does not exist"));
651
652 let status_now = mz_ore::now::to_datetime((self.now)());
653 let mut source_status_updates = vec![];
654 let mut sink_status_updates = vec![];
655
656 let make_update = |id, object_type| StatusUpdate {
659 id,
660 status: Status::Paused,
661 timestamp: status_now,
662 error: None,
663 hints: BTreeSet::from([format!(
664 "The replica running this {object_type} has been dropped"
665 )]),
666 namespaced_errors: Default::default(),
667 replica_id: Some(replica_id),
668 };
669
670 for ingestion_id in instance.active_ingestions() {
671 if let Some(active_replicas) = self.dropped_objects.get_mut(ingestion_id) {
672 active_replicas.remove(&replica_id);
673 if active_replicas.is_empty() {
674 self.dropped_objects.remove(ingestion_id);
675 }
676 }
677
678 let ingestion = self
679 .collections
680 .get_mut(ingestion_id)
681 .expect("instance contains unknown ingestion");
682
683 let ingestion_description = match &ingestion.data_source {
684 DataSource::Ingestion(ingestion_description) => ingestion_description.clone(),
685 _ => panic!(
686 "unexpected data source for ingestion: {:?}",
687 ingestion.data_source
688 ),
689 };
690
691 let old_style_ingestion = *ingestion_id != ingestion_description.remap_collection_id;
692 let subsource_ids = ingestion_description.collection_ids().filter(|id| {
693 let should_discard =
698 old_style_ingestion && id == &ingestion_description.remap_collection_id;
699 !should_discard
700 });
701 for id in subsource_ids {
702 source_status_updates.push(make_update(id, "source"));
703 }
704 }
705
706 for id in instance.active_exports() {
707 if let Some(active_replicas) = self.dropped_objects.get_mut(id) {
708 active_replicas.remove(&replica_id);
709 if active_replicas.is_empty() {
710 self.dropped_objects.remove(id);
711 }
712 }
713
714 sink_status_updates.push(make_update(*id, "sink"));
715 }
716
717 instance.drop_replica(replica_id);
718
719 if !self.read_only {
720 if !source_status_updates.is_empty() {
721 self.append_status_introspection_updates(
722 IntrospectionType::SourceStatusHistory,
723 source_status_updates,
724 );
725 }
726 if !sink_status_updates.is_empty() {
727 self.append_status_introspection_updates(
728 IntrospectionType::SinkStatusHistory,
729 sink_status_updates,
730 );
731 }
732 }
733 }
734
735 async fn evolve_nullability_for_bootstrap(
736 &mut self,
737 storage_metadata: &StorageMetadata,
738 collections: Vec<(GlobalId, RelationDesc)>,
739 ) -> Result<(), StorageError> {
740 let persist_client = self
741 .persist
742 .open(self.persist_location.clone())
743 .await
744 .unwrap();
745
746 for (global_id, relation_desc) in collections {
747 let shard_id = storage_metadata.get_collection_shard(global_id)?;
748 let diagnostics = Diagnostics {
749 shard_name: global_id.to_string(),
750 handle_purpose: "evolve nullability for bootstrap".to_string(),
751 };
752 let latest_schema = persist_client
753 .latest_schema::<SourceData, (), Timestamp, StorageDiff>(shard_id, diagnostics)
754 .await
755 .expect("invalid persist usage");
756 let Some((schema_id, current_schema, _)) = latest_schema else {
757 tracing::debug!(?global_id, "no schema registered");
758 continue;
759 };
760 tracing::debug!(?global_id, ?current_schema, new_schema = ?relation_desc, "migrating schema");
761
762 let diagnostics = Diagnostics {
763 shard_name: global_id.to_string(),
764 handle_purpose: "evolve nullability for bootstrap".to_string(),
765 };
766 let evolve_result = persist_client
767 .compare_and_evolve_schema::<SourceData, (), Timestamp, StorageDiff>(
768 shard_id,
769 schema_id,
770 &relation_desc,
771 &UnitSchema,
772 diagnostics,
773 )
774 .await
775 .expect("invalid persist usage");
776 match evolve_result {
777 CaESchema::Ok(_) => (),
778 CaESchema::ExpectedMismatch {
779 schema_id,
780 key,
781 val: _,
782 } => {
783 return Err(StorageError::PersistSchemaEvolveRace {
784 global_id,
785 shard_id,
786 schema_id,
787 relation_desc: key,
788 });
789 }
790 CaESchema::Incompatible => {
791 return Err(StorageError::PersistInvalidSchemaEvolve {
792 global_id,
793 shard_id,
794 });
795 }
796 };
797 }
798
799 Ok(())
800 }
801
802 #[instrument(name = "storage::create_collections")]
821 async fn create_collections_for_bootstrap(
822 &mut self,
823 storage_metadata: &StorageMetadata,
824 register_ts: Option<Timestamp>,
825 mut collections: Vec<(GlobalId, CollectionDescription)>,
826 migrated_storage_collections: &BTreeSet<GlobalId>,
827 ) -> Result<(), StorageError> {
828 self.migrated_storage_collections
829 .extend(migrated_storage_collections.iter().cloned());
830
831 self.storage_collections
832 .create_collections_for_bootstrap(
833 storage_metadata,
834 register_ts,
835 collections.clone(),
836 migrated_storage_collections,
837 )
838 .await?;
839
840 drop(self.persist_warm_task.take());
843
844 collections.sort_by_key(|(id, _)| *id);
849 collections.dedup();
850 for pos in 1..collections.len() {
851 if collections[pos - 1].0 == collections[pos].0 {
852 return Err(StorageError::CollectionIdReused(collections[pos].0));
853 }
854 }
855
856 let enriched_with_metadata = collections
858 .into_iter()
859 .map(|(id, description)| {
860 let data_shard = storage_metadata.get_collection_shard(id)?;
861
862 let txns_shard = description
865 .data_source
866 .in_txns()
867 .then(|| *self.txns_read.txns_id());
868
869 let metadata = CollectionMetadata {
870 persist_location: self.persist_location.clone(),
871 data_shard,
872 relation_desc: description.desc.clone(),
873 txns_shard,
874 };
875
876 Ok((id, description, metadata))
877 })
878 .collect_vec();
879
880 let persist_client = self
882 .persist
883 .open(self.persist_location.clone())
884 .await
885 .unwrap();
886 let persist_client = &persist_client;
887
888 use futures::stream::{StreamExt, TryStreamExt};
891 let this = &*self;
892 let mut to_register: Vec<_> = futures::stream::iter(enriched_with_metadata)
893 .map(|data: Result<_, StorageError>| {
894 async move {
895 let (id, description, metadata) = data?;
896
897 debug!("mapping GlobalId={} to shard ({})", id, metadata.data_shard);
900
901 let write = if matches!(description.data_source, DataSource::Table) {
906 let diagnostics = Diagnostics {
907 shard_name: id.to_string(),
908 handle_purpose: format!("controller data for {}", id),
909 };
910 let upper = persist_client
911 .recent_upper::<SourceData, (), Timestamp, StorageDiff>(
912 metadata.data_shard,
913 diagnostics,
914 )
915 .await
916 .expect("invalid persist usage");
917 WriteHandleOrUpper::Upper(upper)
918 } else {
919 let write = this
920 .open_data_handles(
921 &id,
922 metadata.data_shard,
923 metadata.relation_desc.clone(),
924 persist_client,
925 )
926 .await;
927 WriteHandleOrUpper::Handle(write)
928 };
929
930 Ok::<_, StorageError>((id, description, write, metadata))
931 }
932 })
933 .buffer_unordered(50)
935 .try_collect()
948 .await?;
949
950 let mut to_execute = BTreeSet::new();
953 let mut new_collections = BTreeSet::new();
958
959 to_register.sort_by_key(|(id, ..)| *id);
961
962 let (tables_to_register, collections_to_register): (Vec<_>, Vec<_>) = to_register
968 .into_iter()
969 .partition(|(_id, desc, ..)| desc.data_source == DataSource::Table);
970 let to_register = tables_to_register
971 .into_iter()
972 .rev()
973 .chain(collections_to_register.into_iter());
974
975 let mut new_webhook_statistic_entries = BTreeSet::new();
979
980 for (id, description, write, metadata) in to_register {
981 let is_in_txns = |id, metadata: &CollectionMetadata| {
982 metadata.txns_shard.is_some()
983 && !(self.read_only && migrated_storage_collections.contains(&id))
984 };
985
986 to_execute.insert(id);
987 new_collections.insert(id);
988
989 let write_frontier = write.upper();
990
991 let storage_dependencies = self.determine_collection_dependencies(id, &description)?;
993
994 let dependency_read_holds = self
995 .storage_collections
996 .acquire_read_holds(storage_dependencies)
997 .expect("can acquire read holds");
998
999 let mut dependency_since = Antichain::from_elem(Timestamp::MIN);
1000 for read_hold in dependency_read_holds.iter() {
1001 dependency_since.join_assign(read_hold.since());
1002 }
1003
1004 let data_source = description.data_source;
1005
1006 if !dependency_read_holds.is_empty()
1015 && !is_in_txns(id, &metadata)
1016 && !matches!(&data_source, DataSource::Sink { .. })
1017 {
1018 if dependency_since.is_empty() && !write_frontier.is_empty() {
1031 halt!(
1032 "dependency since frontier is empty while dependent upper \
1033 is not empty (dependent id={id}, write_frontier={:?}, dependency_read_holds={:?}), \
1034 this indicates concurrent deletion of a collection",
1035 write_frontier,
1036 dependency_read_holds,
1037 );
1038 }
1039
1040 if description.primary.is_none() {
1058 mz_ore::soft_assert_or_log!(
1059 write_frontier.elements() == &[Timestamp::MIN]
1060 || write_frontier.is_empty()
1061 || PartialOrder::less_than(&dependency_since, &write_frontier),
1062 "dependency since has advanced past dependent ({id}) upper \n
1063 dependent ({id}): upper {:?} \n
1064 dependency since {:?} \n
1065 dependency read holds: {:?}",
1066 write_frontier,
1067 dependency_since,
1068 dependency_read_holds,
1069 );
1070 }
1071 }
1072
1073 let mut extra_state = CollectionStateExtra::None;
1075 let mut maybe_instance_id = None;
1076 match &data_source {
1077 DataSource::Introspection(typ) => {
1078 debug!(
1079 ?data_source, meta = ?metadata,
1080 "registering {id} with persist monotonic worker",
1081 );
1082 self.register_introspection_collection(
1088 id,
1089 *typ,
1090 write.expect_handle("introspection collections are not tables"),
1091 persist_client.clone(),
1092 )?;
1093 }
1094 DataSource::Webhook => {
1095 debug!(
1096 ?data_source, meta = ?metadata,
1097 "registering {id} with persist monotonic worker",
1098 );
1099 new_webhook_statistic_entries.insert(id);
1102 self.collection_manager.register_append_only_collection(
1108 id,
1109 write.expect_handle("webhook collections are not tables"),
1110 false,
1111 None,
1112 );
1113 }
1114 DataSource::IngestionExport {
1115 ingestion_id,
1116 details,
1117 data_config,
1118 } => {
1119 debug!(
1120 ?data_source, meta = ?metadata,
1121 "not registering {id} with a controller persist worker",
1122 );
1123 let ingestion_state = self
1125 .collections
1126 .get_mut(ingestion_id)
1127 .expect("known to exist");
1128
1129 let instance_id = match &mut ingestion_state.data_source {
1130 DataSource::Ingestion(ingestion_desc) => {
1131 ingestion_desc.source_exports.insert(
1132 id,
1133 SourceExport {
1134 storage_metadata: (),
1135 details: details.clone(),
1136 data_config: data_config.clone(),
1137 },
1138 );
1139
1140 ingestion_desc.instance_id
1145 }
1146 _ => unreachable!(
1147 "SourceExport must only refer to primary sources that already exist"
1148 ),
1149 };
1150
1151 to_execute.remove(&id);
1153 to_execute.insert(*ingestion_id);
1154
1155 let ingestion_state = IngestionState {
1156 read_capabilities: MutableAntichain::from(dependency_since.clone()),
1157 dependency_read_holds,
1158 derived_since: dependency_since,
1159 write_frontier: Antichain::from_elem(Timestamp::MIN),
1160 hold_policy: ReadPolicy::step_back(),
1161 instance_id,
1162 hydrated_on: BTreeSet::new(),
1163 };
1164
1165 extra_state = CollectionStateExtra::Ingestion(ingestion_state);
1166 maybe_instance_id = Some(instance_id);
1167 }
1168 DataSource::Table => {
1169 debug!(
1170 ?data_source, meta = ?metadata,
1171 "not registering {id} with the txns shard here; the caller does that \
1172 through the group committer",
1173 );
1174 }
1175 DataSource::Progress | DataSource::Other => {
1176 debug!(
1177 ?data_source, meta = ?metadata,
1178 "not registering {id} with a controller persist worker",
1179 );
1180 }
1181 DataSource::Ingestion(ingestion_desc) => {
1182 debug!(
1183 ?data_source, meta = ?metadata,
1184 "not registering {id} with a controller persist worker",
1185 );
1186
1187 let mut dependency_since = Antichain::from_elem(Timestamp::MIN);
1188 for read_hold in dependency_read_holds.iter() {
1189 dependency_since.join_assign(read_hold.since());
1190 }
1191
1192 let ingestion_state = IngestionState {
1193 read_capabilities: MutableAntichain::from(dependency_since.clone()),
1194 dependency_read_holds,
1195 derived_since: dependency_since,
1196 write_frontier: Antichain::from_elem(Timestamp::MIN),
1197 hold_policy: ReadPolicy::step_back(),
1198 instance_id: ingestion_desc.instance_id,
1199 hydrated_on: BTreeSet::new(),
1200 };
1201
1202 extra_state = CollectionStateExtra::Ingestion(ingestion_state);
1203 maybe_instance_id = Some(ingestion_desc.instance_id);
1204 }
1205 DataSource::Sink { desc } => {
1206 let mut dependency_since = Antichain::from_elem(Timestamp::MIN);
1207 for read_hold in dependency_read_holds.iter() {
1208 dependency_since.join_assign(read_hold.since());
1209 }
1210
1211 let [self_hold, read_hold] =
1212 dependency_read_holds.try_into().expect("two holds");
1213
1214 let state = ExportState::new(
1215 desc.instance_id,
1216 read_hold,
1217 self_hold,
1218 write_frontier.clone(),
1219 ReadPolicy::step_back(),
1220 );
1221 maybe_instance_id = Some(state.cluster_id);
1222 extra_state = CollectionStateExtra::Export(state);
1223 }
1224 }
1225
1226 let wallclock_lag_metrics = self.metrics.wallclock_lag_metrics(id, maybe_instance_id);
1227 let collection_state =
1228 CollectionState::new(data_source, metadata, extra_state, wallclock_lag_metrics);
1229
1230 self.collections.insert(id, collection_state);
1231 }
1232
1233 {
1234 let mut source_statistics = self.source_statistics.lock().expect("poisoned");
1235
1236 for id in new_webhook_statistic_entries {
1239 source_statistics.webhook_statistics.entry(id).or_default();
1240 }
1241
1242 }
1246
1247 self.append_shard_mappings(new_collections.into_iter(), Diff::ONE);
1248
1249 for id in to_execute {
1251 match &self.collection(id)?.data_source {
1252 DataSource::Ingestion(ingestion) => {
1253 if !self.read_only
1254 || (ENABLE_0DT_DEPLOYMENT_SOURCES.get(self.config.config_set())
1255 && ingestion.desc.connection.supports_read_only())
1256 {
1257 self.run_ingestion(id)?;
1258 }
1259 }
1260 DataSource::IngestionExport { .. } => unreachable!(
1261 "ingestion exports do not execute directly, but instead schedule their source to be re-executed"
1262 ),
1263 DataSource::Introspection(_)
1264 | DataSource::Webhook
1265 | DataSource::Table
1266 | DataSource::Progress
1267 | DataSource::Other => {}
1268 DataSource::Sink { .. } => {
1269 if !self.read_only {
1270 self.run_export(id)?;
1271 }
1272 }
1273 };
1274 }
1275
1276 Ok(())
1277 }
1278
1279 fn check_alter_ingestion_source_desc(
1280 &mut self,
1281 ingestion_id: GlobalId,
1282 source_desc: &SourceDesc,
1283 ) -> Result<(), StorageError> {
1284 let source_collection = self.collection(ingestion_id)?;
1285 let data_source = &source_collection.data_source;
1286 match &data_source {
1287 DataSource::Ingestion(cur_ingestion) => {
1288 cur_ingestion
1289 .desc
1290 .alter_compatible(ingestion_id, source_desc)?;
1291 }
1292 o => {
1293 tracing::info!(
1294 "{ingestion_id} inalterable because its data source is {:?} and not an ingestion",
1295 o
1296 );
1297 Err(AlterError { id: ingestion_id })?
1298 }
1299 }
1300
1301 Ok(())
1302 }
1303
1304 async fn alter_ingestion_source_desc(
1305 &mut self,
1306 ingestion_ids: BTreeMap<GlobalId, SourceDesc>,
1307 ) -> Result<(), StorageError> {
1308 let mut ingestions_to_run = BTreeSet::new();
1309
1310 for (id, new_desc) in ingestion_ids {
1311 let collection = self
1312 .collections
1313 .get_mut(&id)
1314 .ok_or_else(|| StorageError::IdentifierMissing(id))?;
1315
1316 match &mut collection.data_source {
1317 DataSource::Ingestion(ingestion) => {
1318 if ingestion.desc != new_desc {
1319 tracing::info!(
1320 from = ?ingestion.desc,
1321 to = ?new_desc,
1322 "alter_ingestion_source_desc, updating"
1323 );
1324 ingestion.desc = new_desc;
1325 ingestions_to_run.insert(id);
1326 }
1327 }
1328 o => {
1329 tracing::warn!("alter_ingestion_source_desc called on {:?}", o);
1330 Err(StorageError::IdentifierInvalid(id))?;
1331 }
1332 }
1333 }
1334
1335 for id in ingestions_to_run {
1336 self.run_ingestion(id)?;
1337 }
1338 Ok(())
1339 }
1340
1341 async fn alter_ingestion_connections(
1342 &mut self,
1343 source_connections: BTreeMap<GlobalId, GenericSourceConnection<InlinedConnection>>,
1344 ) -> Result<(), StorageError> {
1345 let mut ingestions_to_run = BTreeSet::new();
1346
1347 for (id, conn) in source_connections {
1348 let collection = self
1349 .collections
1350 .get_mut(&id)
1351 .ok_or_else(|| StorageError::IdentifierMissing(id))?;
1352
1353 match &mut collection.data_source {
1354 DataSource::Ingestion(ingestion) => {
1355 if ingestion.desc.connection != conn {
1358 tracing::info!(from = ?ingestion.desc.connection, to = ?conn, "alter_ingestion_connections, updating");
1359 ingestion.desc.connection = conn;
1360 ingestions_to_run.insert(id);
1361 } else {
1362 tracing::warn!(
1363 "update_source_connection called on {id} but the \
1364 connection was the same"
1365 );
1366 }
1367 }
1368 o => {
1369 tracing::warn!("update_source_connection called on {:?}", o);
1370 Err(StorageError::IdentifierInvalid(id))?;
1371 }
1372 }
1373 }
1374
1375 for id in ingestions_to_run {
1376 self.run_ingestion(id)?;
1377 }
1378 Ok(())
1379 }
1380
1381 async fn alter_ingestion_export_data_configs(
1382 &mut self,
1383 source_exports: BTreeMap<GlobalId, SourceExportDataConfig>,
1384 ) -> Result<(), StorageError> {
1385 let mut ingestions_to_run = BTreeSet::new();
1386
1387 for (source_export_id, new_data_config) in source_exports {
1388 let source_export_collection = self
1391 .collections
1392 .get_mut(&source_export_id)
1393 .ok_or_else(|| StorageError::IdentifierMissing(source_export_id))?;
1394 let ingestion_id = match &mut source_export_collection.data_source {
1395 DataSource::IngestionExport {
1396 ingestion_id,
1397 details: _,
1398 data_config,
1399 } => {
1400 *data_config = new_data_config.clone();
1401 *ingestion_id
1402 }
1403 o => {
1404 tracing::warn!("alter_ingestion_export_data_configs called on {:?}", o);
1405 Err(StorageError::IdentifierInvalid(source_export_id))?
1406 }
1407 };
1408 let ingestion_collection = self
1411 .collections
1412 .get_mut(&ingestion_id)
1413 .ok_or_else(|| StorageError::IdentifierMissing(ingestion_id))?;
1414
1415 match &mut ingestion_collection.data_source {
1416 DataSource::Ingestion(ingestion_desc) => {
1417 let source_export = ingestion_desc
1418 .source_exports
1419 .get_mut(&source_export_id)
1420 .ok_or_else(|| StorageError::IdentifierMissing(source_export_id))?;
1421
1422 if source_export.data_config != new_data_config {
1425 tracing::info!(?source_export_id, from = ?source_export.data_config, to = ?new_data_config, "alter_ingestion_export_data_configs, updating");
1426 source_export.data_config = new_data_config;
1427
1428 ingestions_to_run.insert(ingestion_id);
1429 } else {
1430 tracing::warn!(
1431 "alter_ingestion_export_data_configs called on \
1432 export {source_export_id} of {ingestion_id} but \
1433 the data config was the same"
1434 );
1435 }
1436 }
1437 o => {
1438 tracing::warn!("alter_ingestion_export_data_configs called on {:?}", o);
1439 Err(StorageError::IdentifierInvalid(ingestion_id))?
1440 }
1441 }
1442 }
1443
1444 for id in ingestions_to_run {
1445 self.run_ingestion(id)?;
1446 }
1447 Ok(())
1448 }
1449
1450 async fn alter_table_desc(
1451 &mut self,
1452 existing_collection: GlobalId,
1453 new_collection: GlobalId,
1454 new_desc: RelationDesc,
1455 expected_version: RelationVersion,
1456 ) -> Result<(), StorageError> {
1457 let data_shard = {
1458 let Controller {
1459 collections,
1460 storage_collections,
1461 ..
1462 } = self;
1463
1464 let existing = collections
1465 .get(&existing_collection)
1466 .ok_or(StorageError::IdentifierMissing(existing_collection))?;
1467 if existing.data_source != DataSource::Table {
1468 return Err(StorageError::IdentifierInvalid(existing_collection));
1469 }
1470
1471 storage_collections
1473 .alter_table_desc(
1474 existing_collection,
1475 new_collection,
1476 new_desc.clone(),
1477 expected_version,
1478 )
1479 .await?;
1480
1481 existing.collection_metadata.data_shard.clone()
1482 };
1483
1484 let collection_meta = CollectionMetadata {
1485 persist_location: self.persist_location.clone(),
1486 data_shard,
1487 relation_desc: new_desc.clone(),
1488 txns_shard: Some(self.txns_read.txns_id().clone()),
1490 };
1491 let wallclock_lag_metrics = self.metrics.wallclock_lag_metrics(new_collection, None);
1493 let collection_state = CollectionState::new(
1494 DataSource::Table,
1495 collection_meta,
1496 CollectionStateExtra::None,
1497 wallclock_lag_metrics,
1498 );
1499
1500 self.collections.insert(new_collection, collection_state);
1503
1504 self.append_shard_mappings([new_collection].into_iter(), Diff::ONE);
1505
1506 Ok(())
1507 }
1508
1509 async fn register_table_collections(
1510 &mut self,
1511 register_ts: Timestamp,
1512 ids: Vec<GlobalId>,
1513 ) -> Result<(), StorageError> {
1514 let mut tables = self.table_registrations(ids)?;
1515
1516 if self.read_only {
1518 tables.retain(|table| self.migrated_storage_collections.contains(&table.id));
1519 }
1520 if tables.is_empty() {
1521 return Ok(());
1522 }
1523
1524 match self
1525 .persist_table_worker
1526 .register(register_ts, tables)
1527 .await
1528 {
1529 Ok(res) => res,
1530 Err(_recv) => Err(StorageError::ShuttingDown("persist_table_worker")),
1531 }
1532 }
1533
1534 fn table_registrations(
1535 &self,
1536 ids: Vec<GlobalId>,
1537 ) -> Result<Vec<TableRegistration>, StorageError> {
1538 let mut tables = Vec::with_capacity(ids.len());
1540 for id in ids {
1541 let collection = self.collection(id)?;
1542 if matches!(collection.data_source, DataSource::Table) {
1543 let metadata = &collection.collection_metadata;
1544 tables.push(TableRegistration {
1545 id,
1546 data_shard: metadata.data_shard,
1547 relation_desc: metadata.relation_desc.clone(),
1548 });
1549 }
1550 }
1551 Ok(tables)
1552 }
1553
1554 fn txns_table_ids(&self, ids: Vec<GlobalId>) -> Result<Vec<GlobalId>, StorageError> {
1555 let mut tables = Vec::with_capacity(ids.len());
1556 for id in ids {
1557 let collection = self.collection(id)?;
1558 if matches!(collection.data_source, DataSource::Table) {
1559 tables.push(id);
1560 }
1561 }
1562 Ok(tables)
1563 }
1564
1565 fn export(&self, id: GlobalId) -> Result<&ExportState, StorageError> {
1566 self.collections
1567 .get(&id)
1568 .and_then(|c| match &c.extra_state {
1569 CollectionStateExtra::Export(state) => Some(state),
1570 _ => None,
1571 })
1572 .ok_or(StorageError::IdentifierMissing(id))
1573 }
1574
1575 fn export_mut(&mut self, id: GlobalId) -> Result<&mut ExportState, StorageError> {
1576 self.collections
1577 .get_mut(&id)
1578 .and_then(|c| match &mut c.extra_state {
1579 CollectionStateExtra::Export(state) => Some(state),
1580 _ => None,
1581 })
1582 .ok_or(StorageError::IdentifierMissing(id))
1583 }
1584
1585 async fn create_oneshot_ingestion(
1587 &mut self,
1588 ingestion_id: uuid::Uuid,
1589 collection_id: GlobalId,
1590 instance_id: StorageInstanceId,
1591 request: OneshotIngestionRequest,
1592 result_tx: OneshotResultCallback<ProtoBatch>,
1593 ) -> Result<(), StorageError> {
1594 let collection_meta = self
1595 .collections
1596 .get(&collection_id)
1597 .ok_or_else(|| StorageError::IdentifierMissing(collection_id))?
1598 .collection_metadata
1599 .clone();
1600 let instance = self.instances.get_mut(&instance_id).ok_or_else(|| {
1601 StorageError::Generic(anyhow::anyhow!("missing cluster {instance_id}"))
1603 })?;
1604 let oneshot_cmd = RunOneshotIngestion {
1605 ingestion_id,
1606 collection_id,
1607 collection_meta,
1608 request,
1609 };
1610
1611 if !self.read_only {
1612 instance.send(StorageCommand::RunOneshotIngestion(Box::new(oneshot_cmd)));
1613 let pending = PendingOneshotIngestion {
1614 result_tx,
1615 cluster_id: instance_id,
1616 };
1617 let novel = self
1618 .pending_oneshot_ingestions
1619 .insert(ingestion_id, pending);
1620 assert_none!(novel);
1621 Ok(())
1622 } else {
1623 Err(StorageError::ReadOnly)
1624 }
1625 }
1626
1627 fn cancel_oneshot_ingestion(&mut self, ingestion_id: uuid::Uuid) -> Result<(), StorageError> {
1628 if self.read_only {
1629 return Err(StorageError::ReadOnly);
1630 }
1631
1632 let pending = self
1633 .pending_oneshot_ingestions
1634 .remove(&ingestion_id)
1635 .ok_or_else(|| {
1636 StorageError::Generic(anyhow::anyhow!("missing oneshot ingestion {ingestion_id}"))
1638 })?;
1639
1640 match self.instances.get_mut(&pending.cluster_id) {
1641 Some(instance) => {
1642 instance.send(StorageCommand::CancelOneshotIngestion(ingestion_id));
1643 }
1644 None => {
1645 mz_ore::soft_panic_or_log!(
1646 "canceling oneshot ingestion on non-existent cluster, ingestion {:?}, instance {}",
1647 ingestion_id,
1648 pending.cluster_id,
1649 );
1650 }
1651 }
1652 pending.cancel();
1654
1655 Ok(())
1656 }
1657
1658 async fn alter_export(
1659 &mut self,
1660 id: GlobalId,
1661 new_description: ExportDescription,
1662 ) -> Result<(), StorageError> {
1663 let from_id = new_description.sink.from;
1664
1665 let desired_read_holds = vec![from_id.clone(), id.clone()];
1668 let [input_hold, self_hold] = self
1669 .storage_collections
1670 .acquire_read_holds(desired_read_holds)
1671 .expect("missing dependency")
1672 .try_into()
1673 .expect("expected number of holds");
1674 let from_storage_metadata = self.storage_collections.collection_metadata(from_id)?;
1675 let to_storage_metadata = self.storage_collections.collection_metadata(id)?;
1676
1677 let cur_export = self.export_mut(id)?;
1679 let input_readable = cur_export
1680 .write_frontier
1681 .iter()
1682 .all(|t| input_hold.since().less_than(t));
1683 if !input_readable {
1684 return Err(StorageError::ReadBeforeSince(from_id));
1685 }
1686
1687 let new_export = ExportState {
1688 read_capabilities: cur_export.read_capabilities.clone(),
1689 cluster_id: new_description.instance_id,
1690 derived_since: cur_export.derived_since.clone(),
1691 read_holds: [input_hold, self_hold],
1692 read_policy: cur_export.read_policy.clone(),
1693 write_frontier: cur_export.write_frontier.clone(),
1694 };
1695 *cur_export = new_export;
1696
1697 let with_snapshot = new_description.sink.with_snapshot
1704 && !PartialOrder::less_than(&new_description.sink.as_of, &cur_export.write_frontier);
1705
1706 let cmd = RunSinkCommand {
1707 id,
1708 description: StorageSinkDesc {
1709 from: from_id,
1710 from_desc: new_description.sink.from_desc.clone(),
1711 connection: new_description.sink.connection.clone(),
1712 envelope: new_description.sink.envelope,
1713 as_of: new_description.sink.as_of.clone(),
1714 version: new_description.sink.version,
1715 from_storage_metadata,
1716 with_snapshot,
1717 to_storage_metadata,
1718 commit_interval: new_description.sink.commit_interval,
1719 },
1720 };
1721
1722 let instance = self
1724 .instances
1725 .get_mut(&new_description.instance_id)
1726 .ok_or_else(|| StorageError::ExportInstanceMissing {
1727 storage_instance_id: new_description.instance_id,
1728 export_id: id,
1729 })?;
1730
1731 instance.send(StorageCommand::RunSink(Box::new(cmd)));
1732
1733 let state = self
1737 .collections
1738 .get_mut(&id)
1739 .expect("export known to exist");
1740 let DataSource::Sink { desc } = &mut state.data_source else {
1741 panic!("export known to exist")
1742 };
1743 *desc = new_description;
1744
1745 Ok(())
1746 }
1747
1748 async fn alter_export_connections(
1750 &mut self,
1751 exports: BTreeMap<GlobalId, StorageSinkConnection>,
1752 ) -> Result<(), StorageError> {
1753 let mut updates_by_instance =
1754 BTreeMap::<StorageInstanceId, Vec<(RunSinkCommand, ExportDescription)>>::new();
1755
1756 for (id, connection) in exports {
1757 let (mut new_export_description, as_of): (ExportDescription, _) = {
1765 let export = &self.collections[&id];
1766 let DataSource::Sink { desc } = &export.data_source else {
1767 panic!("export exists")
1768 };
1769 let CollectionStateExtra::Export(state) = &export.extra_state else {
1770 panic!("export exists")
1771 };
1772 let export_description = desc.clone();
1773 let as_of = state.input_hold().since().clone();
1774
1775 (export_description, as_of)
1776 };
1777 let current_sink = new_export_description.sink.clone();
1778
1779 new_export_description.sink.connection = connection;
1780
1781 current_sink.alter_compatible(id, &new_export_description.sink)?;
1783
1784 let from_storage_metadata = self
1785 .storage_collections
1786 .collection_metadata(new_export_description.sink.from)?;
1787 let to_storage_metadata = self.storage_collections.collection_metadata(id)?;
1788
1789 let cmd = RunSinkCommand {
1790 id,
1791 description: StorageSinkDesc {
1792 from: new_export_description.sink.from,
1793 from_desc: new_export_description.sink.from_desc.clone(),
1794 connection: new_export_description.sink.connection.clone(),
1795 envelope: new_export_description.sink.envelope,
1796 with_snapshot: new_export_description.sink.with_snapshot,
1797 version: new_export_description.sink.version,
1798 as_of: as_of.to_owned(),
1809 from_storage_metadata,
1810 to_storage_metadata,
1811 commit_interval: new_export_description.sink.commit_interval,
1812 },
1813 };
1814
1815 let update = updates_by_instance
1816 .entry(new_export_description.instance_id)
1817 .or_default();
1818 update.push((cmd, new_export_description));
1819 }
1820
1821 for (instance_id, updates) in updates_by_instance {
1822 let mut export_updates = BTreeMap::new();
1823 let mut cmds = Vec::with_capacity(updates.len());
1824
1825 for (cmd, export_state) in updates {
1826 export_updates.insert(cmd.id, export_state);
1827 cmds.push(cmd);
1828 }
1829
1830 let instance = self.instances.get_mut(&instance_id).ok_or_else(|| {
1832 StorageError::ExportInstanceMissing {
1833 storage_instance_id: instance_id,
1834 export_id: *export_updates
1835 .keys()
1836 .next()
1837 .expect("set of exports not empty"),
1838 }
1839 })?;
1840
1841 for cmd in cmds {
1842 instance.send(StorageCommand::RunSink(Box::new(cmd)));
1843 }
1844
1845 for (id, new_export_description) in export_updates {
1847 let Some(state) = self.collections.get_mut(&id) else {
1848 panic!("export known to exist")
1849 };
1850 let DataSource::Sink { desc } = &mut state.data_source else {
1851 panic!("export known to exist")
1852 };
1853 *desc = new_export_description;
1854 }
1855 }
1856
1857 Ok(())
1858 }
1859
1860 fn drop_tables(
1861 &mut self,
1862 storage_metadata: &StorageMetadata,
1863 identifiers: Vec<GlobalId>,
1864 ) -> Result<(), StorageError> {
1865 let (table_write_ids, data_source_ids): (Vec<_>, Vec<_>) = identifiers
1866 .into_iter()
1867 .partition(|id| match self.collections[id].data_source {
1868 DataSource::Table => true,
1869 DataSource::IngestionExport { .. } | DataSource::Webhook => false,
1870 _ => panic!("identifier is not a table: {}", id),
1871 });
1872
1873 if table_write_ids.len() > 0 {
1874 let tx = self.pending_table_handle_drops_tx.clone();
1875 for identifier in table_write_ids {
1876 let _ = tx.send(identifier);
1877 }
1878 }
1879
1880 if data_source_ids.len() > 0 {
1881 self.validate_collection_ids(data_source_ids.iter().cloned())?;
1882 self.drop_sources_unvalidated(storage_metadata, data_source_ids)?;
1883 }
1884
1885 Ok(())
1886 }
1887
1888 fn drop_sources(
1889 &mut self,
1890 storage_metadata: &StorageMetadata,
1891 identifiers: Vec<GlobalId>,
1892 ) -> Result<(), StorageError> {
1893 self.validate_collection_ids(identifiers.iter().cloned())?;
1894 self.drop_sources_unvalidated(storage_metadata, identifiers)
1895 }
1896
1897 fn drop_sources_unvalidated(
1898 &mut self,
1899 storage_metadata: &StorageMetadata,
1900 ids: Vec<GlobalId>,
1901 ) -> Result<(), StorageError> {
1902 let mut ingestions_to_execute = BTreeSet::new();
1905 let mut ingestions_to_drop = BTreeSet::new();
1906 let mut source_statistics_to_drop = Vec::new();
1907
1908 let mut collections_to_drop = Vec::new();
1912
1913 for id in ids.iter() {
1914 let collection_state = self.collections.get(id);
1915
1916 if let Some(collection_state) = collection_state {
1917 match collection_state.data_source {
1918 DataSource::Webhook => {
1919 let fut = self.collection_manager.unregister_collection(*id);
1922 mz_ore::task::spawn(|| format!("storage-webhook-cleanup-{id}"), fut);
1923
1924 collections_to_drop.push(*id);
1925 source_statistics_to_drop.push(*id);
1926 }
1927 DataSource::Ingestion(_) => {
1928 ingestions_to_drop.insert(*id);
1929 source_statistics_to_drop.push(*id);
1930 }
1931 DataSource::IngestionExport { ingestion_id, .. } => {
1932 ingestions_to_execute.insert(ingestion_id);
1939
1940 let ingestion_state = match self.collections.get_mut(&ingestion_id) {
1942 Some(ingestion_collection) => ingestion_collection,
1943 None => {
1945 tracing::error!(
1946 "primary source {ingestion_id} seemingly dropped before subsource {id}"
1947 );
1948 continue;
1949 }
1950 };
1951
1952 match &mut ingestion_state.data_source {
1953 DataSource::Ingestion(ingestion_desc) => {
1954 let removed = ingestion_desc.source_exports.remove(id);
1955 mz_ore::soft_assert_or_log!(
1956 removed.is_some(),
1957 "dropped subsource {id} already removed from source exports"
1958 );
1959 }
1960 _ => unreachable!(
1961 "SourceExport must only refer to primary sources that already exist"
1962 ),
1963 };
1964
1965 ingestions_to_drop.insert(*id);
1969 source_statistics_to_drop.push(*id);
1970 }
1971 DataSource::Progress | DataSource::Table | DataSource::Other => {
1972 collections_to_drop.push(*id);
1973 }
1974 DataSource::Introspection(_) | DataSource::Sink { .. } => {
1975 soft_panic_or_log!(
1978 "drop_sources called on a {:?} (id={id}))",
1979 collection_state.data_source,
1980 );
1981 }
1982 }
1983 }
1984 }
1985
1986 ingestions_to_execute.retain(|id| !ingestions_to_drop.contains(id));
1988 for ingestion_id in ingestions_to_execute {
1989 self.run_ingestion(ingestion_id)?;
1990 }
1991
1992 let ingestion_policies = ingestions_to_drop
1999 .iter()
2000 .map(|id| (*id, ReadPolicy::ValidFrom(Antichain::new())))
2001 .collect();
2002
2003 tracing::debug!(
2004 ?ingestion_policies,
2005 "dropping sources by setting read hold policies"
2006 );
2007 self.set_hold_policies(ingestion_policies);
2008
2009 let shards_to_update: BTreeSet<_> = ingestions_to_drop
2011 .iter()
2012 .chain(collections_to_drop.iter())
2013 .cloned()
2014 .collect();
2015 self.append_shard_mappings(shards_to_update.into_iter(), Diff::MINUS_ONE);
2016
2017 let status_now = mz_ore::now::to_datetime((self.now)());
2018 let mut status_updates = vec![];
2019 for id in ingestions_to_drop.iter() {
2020 status_updates.push(StatusUpdate::new(*id, status_now, Status::Dropped));
2021 }
2022
2023 if !self.read_only {
2024 self.append_status_introspection_updates(
2025 IntrospectionType::SourceStatusHistory,
2026 status_updates,
2027 );
2028 }
2029
2030 {
2031 let mut source_statistics = self.source_statistics.lock().expect("poisoned");
2032 for id in source_statistics_to_drop {
2033 source_statistics
2034 .source_statistics
2035 .retain(|(stats_id, _), _| stats_id != &id);
2036 source_statistics
2037 .webhook_statistics
2038 .retain(|stats_id, _| stats_id != &id);
2039 }
2040 }
2041
2042 for id in ingestions_to_drop.iter().chain(collections_to_drop.iter()) {
2044 tracing::info!(%id, "dropping collection state");
2045 let collection = self
2046 .collections
2047 .remove(id)
2048 .expect("list populated after checking that self.collections contains it");
2049
2050 let instance = match &collection.extra_state {
2051 CollectionStateExtra::Ingestion(ingestion) => Some(ingestion.instance_id),
2052 CollectionStateExtra::Export(export) => Some(export.cluster_id()),
2053 CollectionStateExtra::None => None,
2054 }
2055 .and_then(|i| self.instances.get(&i));
2056
2057 if let Some(instance) = instance {
2061 let active_replicas = instance.get_active_replicas_for_object(id);
2062 if !active_replicas.is_empty() {
2063 match &collection.data_source {
2070 DataSource::Ingestion(ingestion_desc) => {
2071 if *id != ingestion_desc.remap_collection_id {
2072 self.dropped_objects.insert(
2073 ingestion_desc.remap_collection_id,
2074 active_replicas.clone(),
2075 );
2076 }
2077 }
2078 _ => {}
2079 }
2080
2081 self.dropped_objects.insert(*id, active_replicas);
2082 }
2083 }
2084 }
2085
2086 self.storage_collections
2088 .drop_collections_unvalidated(storage_metadata, ids);
2089
2090 Ok(())
2091 }
2092
2093 fn drop_sinks(
2095 &mut self,
2096 storage_metadata: &StorageMetadata,
2097 identifiers: Vec<GlobalId>,
2098 ) -> Result<(), StorageError> {
2099 self.validate_export_ids(identifiers.iter().cloned())?;
2100 self.drop_sinks_unvalidated(storage_metadata, identifiers);
2101 Ok(())
2102 }
2103
2104 fn drop_sinks_unvalidated(
2105 &mut self,
2106 storage_metadata: &StorageMetadata,
2107 mut sinks_to_drop: Vec<GlobalId>,
2108 ) {
2109 sinks_to_drop.retain(|id| self.export(*id).is_ok());
2111
2112 let drop_policy = sinks_to_drop
2119 .iter()
2120 .map(|id| (*id, ReadPolicy::ValidFrom(Antichain::new())))
2121 .collect();
2122
2123 tracing::debug!(
2124 ?drop_policy,
2125 "dropping sources by setting read hold policies"
2126 );
2127 self.set_hold_policies(drop_policy);
2128
2129 let status_now = mz_ore::now::to_datetime((self.now)());
2136
2137 let mut status_updates = vec![];
2139 {
2140 let mut sink_statistics = self.sink_statistics.lock().expect("poisoned");
2141 for id in sinks_to_drop.iter() {
2142 status_updates.push(StatusUpdate::new(*id, status_now, Status::Dropped));
2143 sink_statistics.retain(|(stats_id, _), _| stats_id != id);
2144 }
2145 }
2146
2147 if !self.read_only {
2148 self.append_status_introspection_updates(
2149 IntrospectionType::SinkStatusHistory,
2150 status_updates,
2151 );
2152 }
2153
2154 for id in sinks_to_drop.iter() {
2156 tracing::info!(%id, "dropping export state");
2157 let collection = self
2158 .collections
2159 .remove(id)
2160 .expect("list populated after checking that self.collections contains it");
2161
2162 let instance = match &collection.extra_state {
2163 CollectionStateExtra::Ingestion(ingestion) => Some(ingestion.instance_id),
2164 CollectionStateExtra::Export(export) => Some(export.cluster_id()),
2165 CollectionStateExtra::None => None,
2166 }
2167 .and_then(|i| self.instances.get(&i));
2168
2169 if let Some(instance) = instance {
2173 let active_replicas = instance.get_active_replicas_for_object(id);
2174 if !active_replicas.is_empty() {
2175 self.dropped_objects.insert(*id, active_replicas);
2176 }
2177 }
2178 }
2179
2180 self.storage_collections
2182 .drop_collections_unvalidated(storage_metadata, sinks_to_drop);
2183 }
2184
2185 #[instrument(level = "debug")]
2186 fn append_table(
2187 &mut self,
2188 write_ts: Timestamp,
2189 advance_to: Timestamp,
2190 commands: Vec<(GlobalId, Vec<TableData>)>,
2191 ) -> Result<tokio::sync::oneshot::Receiver<Result<(), StorageError>>, StorageError> {
2192 if self.read_only {
2193 if !commands
2196 .iter()
2197 .all(|(id, _)| id.is_system() && self.migrated_storage_collections.contains(id))
2198 {
2199 return Err(StorageError::ReadOnly);
2200 }
2201 }
2202
2203 for (id, updates) in commands.iter() {
2205 if !updates.is_empty() {
2206 if !write_ts.less_than(&advance_to) {
2207 return Err(StorageError::UpdateBeyondUpper(*id));
2208 }
2209 }
2210 }
2211
2212 Ok(self
2213 .persist_table_worker
2214 .append(write_ts, advance_to, commands))
2215 }
2216
2217 fn table_write_handle(&self) -> Arc<dyn mz_storage_client::controller::TableWriteHandle> {
2218 Arc::new(persist_handles::TableWriteWorkerHandle(
2219 self.persist_table_worker.clone(),
2220 ))
2221 }
2222
2223 fn monotonic_appender(&self, id: GlobalId) -> Result<MonotonicAppender, StorageError> {
2224 self.collection_manager.monotonic_appender(id)
2225 }
2226
2227 fn webhook_statistics(&self, id: GlobalId) -> Result<Arc<WebhookStatistics>, StorageError> {
2228 let source_statistics = self.source_statistics.lock().expect("poisoned");
2230 source_statistics
2231 .webhook_statistics
2232 .get(&id)
2233 .cloned()
2234 .ok_or(StorageError::IdentifierMissing(id))
2235 }
2236
2237 async fn ready(&mut self) {
2238 if self.maintenance_scheduled {
2239 return;
2240 }
2241
2242 if !self.pending_table_handle_drops_rx.is_empty() {
2243 return;
2244 }
2245
2246 tokio::select! {
2247 Some(m) = self.instance_response_rx.recv() => {
2248 self.stashed_responses.push(m);
2249 while let Ok(m) = self.instance_response_rx.try_recv() {
2250 self.stashed_responses.push(m);
2251 }
2252 }
2253 _ = self.maintenance_ticker.tick() => {
2254 self.maintenance_scheduled = true;
2255 },
2256 };
2257 }
2258
2259 #[instrument(level = "debug")]
2260 fn process(
2261 &mut self,
2262 storage_metadata: &StorageMetadata,
2263 ) -> Result<Option<Response>, anyhow::Error> {
2264 if self.maintenance_scheduled {
2266 self.maintain();
2267 self.maintenance_scheduled = false;
2268 }
2269
2270 for instance in self.instances.values_mut() {
2271 instance.rehydrate_failed_replicas();
2272 }
2273
2274 let mut status_updates = vec![];
2275 let mut updated_frontiers = BTreeMap::new();
2276
2277 let stashed_responses = std::mem::take(&mut self.stashed_responses);
2279 for resp in stashed_responses {
2280 match resp {
2281 (_replica_id, StorageResponse::FrontierUpper(id, upper)) => {
2282 self.update_write_frontier(id, &upper);
2283 updated_frontiers.insert(id, upper);
2284 }
2285 (replica_id, StorageResponse::DroppedId(id)) => {
2286 let replica_id = replica_id.expect("DroppedId from unknown replica");
2287 if let Some(remaining_replicas) = self.dropped_objects.get_mut(&id) {
2288 remaining_replicas.remove(&replica_id);
2289 if remaining_replicas.is_empty() {
2290 self.dropped_objects.remove(&id);
2291 }
2292 } else {
2293 soft_panic_or_log!("unexpected DroppedId for {id}");
2294 }
2295 }
2296 (replica_id, StorageResponse::StatisticsUpdates(source_stats, sink_stats)) => {
2297 {
2299 let replica_id = if let Some(replica_id) = replica_id {
2306 replica_id
2307 } else {
2308 tracing::error!(
2309 ?source_stats,
2310 "missing replica_id for source statistics update"
2311 );
2312 continue;
2313 };
2314
2315 let mut shared_stats = self.source_statistics.lock().expect("poisoned");
2316
2317 for stat in source_stats {
2318 let collection_id = stat.id.clone();
2319
2320 if self.collection(collection_id).is_err() {
2321 continue;
2324 }
2325
2326 let entry = shared_stats
2327 .source_statistics
2328 .entry((stat.id, Some(replica_id)));
2329
2330 match entry {
2331 btree_map::Entry::Vacant(vacant_entry) => {
2332 let mut stats = ControllerSourceStatistics::new(
2333 collection_id,
2334 Some(replica_id),
2335 );
2336 stats.incorporate(stat);
2337 vacant_entry.insert(stats);
2338 }
2339 btree_map::Entry::Occupied(mut occupied_entry) => {
2340 occupied_entry.get_mut().incorporate(stat);
2341 }
2342 }
2343 }
2344 }
2345
2346 {
2347 let replica_id = if let Some(replica_id) = replica_id {
2358 replica_id
2359 } else {
2360 tracing::error!(
2361 ?sink_stats,
2362 "missing replica_id for sink statistics update"
2363 );
2364 continue;
2365 };
2366
2367 let mut shared_stats = self.sink_statistics.lock().expect("poisoned");
2368
2369 for stat in sink_stats {
2370 let collection_id = stat.id.clone();
2371
2372 if self.collection(collection_id).is_err() {
2373 continue;
2376 }
2377
2378 let entry = shared_stats.entry((stat.id, Some(replica_id)));
2379
2380 match entry {
2381 btree_map::Entry::Vacant(vacant_entry) => {
2382 let mut stats =
2383 ControllerSinkStatistics::new(collection_id, replica_id);
2384 stats.incorporate(stat);
2385 vacant_entry.insert(stats);
2386 }
2387 btree_map::Entry::Occupied(mut occupied_entry) => {
2388 occupied_entry.get_mut().incorporate(stat);
2389 }
2390 }
2391 }
2392 }
2393 }
2394 (replica_id, StorageResponse::StatusUpdate(mut status_update)) => {
2395 match status_update.status {
2411 Status::Running => {
2412 let collection = self.collections.get_mut(&status_update.id);
2413 match collection {
2414 Some(collection) => {
2415 match collection.extra_state {
2416 CollectionStateExtra::Ingestion(
2417 ref mut ingestion_state,
2418 ) => {
2419 if ingestion_state.hydrated_on.is_empty() {
2420 tracing::debug!(ingestion_id = %status_update.id, "ingestion is hydrated");
2421 }
2422 ingestion_state.hydrated_on.insert(replica_id.expect(
2423 "replica id should be present for status running",
2424 ));
2425 }
2426 CollectionStateExtra::Export(_) => {
2427 }
2429 CollectionStateExtra::None => {
2430 }
2432 }
2433 }
2434 None => (), }
2437 }
2438 Status::Paused => {
2439 let collection = self.collections.get_mut(&status_update.id);
2440 match collection {
2441 Some(collection) => {
2442 match collection.extra_state {
2443 CollectionStateExtra::Ingestion(
2444 ref mut ingestion_state,
2445 ) => {
2446 tracing::debug!(ingestion_id = %status_update.id, "ingestion is now paused");
2453 ingestion_state.hydrated_on.clear();
2454 }
2455 CollectionStateExtra::Export(_) => {
2456 }
2458 CollectionStateExtra::None => {
2459 }
2461 }
2462 }
2463 None => (), }
2466 }
2467 _ => (),
2468 }
2469
2470 if let Some(id) = replica_id {
2472 status_update.replica_id = Some(id);
2473 }
2474 status_updates.push(status_update);
2475 }
2476 (_replica_id, StorageResponse::StagedBatches(batches)) => {
2477 for (ingestion_id, batches) in batches {
2478 match self.pending_oneshot_ingestions.remove(&ingestion_id) {
2479 Some(pending) => {
2480 if let Some(instance) = self.instances.get_mut(&pending.cluster_id)
2483 {
2484 instance
2485 .send(StorageCommand::CancelOneshotIngestion(ingestion_id));
2486 }
2487 (pending.result_tx)(batches)
2489 }
2490 None => {
2491 }
2494 }
2495 }
2496 }
2497 }
2498 }
2499
2500 self.record_status_updates(status_updates);
2501
2502 let mut dropped_table_ids = Vec::new();
2504 while let Ok(dropped_id) = self.pending_table_handle_drops_rx.try_recv() {
2505 dropped_table_ids.push(dropped_id);
2506 }
2507 if !dropped_table_ids.is_empty() {
2508 self.drop_sources(storage_metadata, dropped_table_ids)?;
2509 }
2510
2511 if updated_frontiers.is_empty() {
2512 Ok(None)
2513 } else {
2514 Ok(Some(Response::FrontierUpdates(
2515 updated_frontiers.into_iter().collect(),
2516 )))
2517 }
2518 }
2519
2520 async fn inspect_persist_state(
2521 &self,
2522 id: GlobalId,
2523 ) -> Result<serde_json::Value, anyhow::Error> {
2524 let collection = &self.storage_collections.collection_metadata(id)?;
2525 let client = self
2526 .persist
2527 .open(collection.persist_location.clone())
2528 .await?;
2529 let shard_state = client
2530 .inspect_shard::<Timestamp>(&collection.data_shard)
2531 .await?;
2532 let json_state = serde_json::to_value(shard_state)?;
2533 Ok(json_state)
2534 }
2535
2536 fn append_introspection_updates(
2537 &mut self,
2538 type_: IntrospectionType,
2539 updates: Vec<(Row, Diff)>,
2540 ) {
2541 let id = self.introspection_ids[&type_];
2542 let updates = updates.into_iter().map(|update| update.into()).collect();
2543 self.collection_manager.blind_write(id, updates);
2544 }
2545
2546 fn append_status_introspection_updates(
2547 &mut self,
2548 type_: IntrospectionType,
2549 updates: Vec<StatusUpdate>,
2550 ) {
2551 let id = self.introspection_ids[&type_];
2552 let updates: Vec<_> = updates.into_iter().map(|update| update.into()).collect();
2553 if !updates.is_empty() {
2554 self.collection_manager.blind_write(id, updates);
2555 }
2556 }
2557
2558 fn update_introspection_collection(&mut self, type_: IntrospectionType, op: StorageWriteOp) {
2559 let id = self.introspection_ids[&type_];
2560 self.collection_manager.differential_write(id, op);
2561 }
2562
2563 fn append_only_introspection_tx(
2564 &self,
2565 type_: IntrospectionType,
2566 ) -> mpsc::UnboundedSender<(
2567 Vec<AppendOnlyUpdate>,
2568 oneshot::Sender<Result<(), StorageError>>,
2569 )> {
2570 let id = self.introspection_ids[&type_];
2571 self.collection_manager.append_only_write_sender(id)
2572 }
2573
2574 fn differential_introspection_tx(
2575 &self,
2576 type_: IntrospectionType,
2577 ) -> mpsc::UnboundedSender<(StorageWriteOp, oneshot::Sender<Result<(), StorageError>>)> {
2578 let id = self.introspection_ids[&type_];
2579 self.collection_manager.differential_write_sender(id)
2580 }
2581
2582 async fn real_time_recent_timestamp(
2583 &self,
2584 timestamp_objects: BTreeSet<GlobalId>,
2585 timeout: Duration,
2586 ) -> Result<BoxFuture<Result<Timestamp, StorageError>>, StorageError> {
2587 use mz_storage_types::sources::GenericSourceConnection;
2588
2589 let mut rtr_futures = BTreeMap::new();
2590
2591 for id in timestamp_objects.into_iter().filter(GlobalId::is_user) {
2593 let collection = match self.collection(id) {
2594 Ok(c) => c,
2595 Err(_) => continue,
2597 };
2598
2599 let (source_conn, remap_id) = match &collection.data_source {
2600 DataSource::Ingestion(IngestionDescription {
2601 desc: SourceDesc { connection, .. },
2602 remap_collection_id,
2603 ..
2604 }) => match connection {
2605 GenericSourceConnection::Kafka(_)
2606 | GenericSourceConnection::Postgres(_)
2607 | GenericSourceConnection::MySql(_)
2608 | GenericSourceConnection::SqlServer(_) => {
2609 (connection.clone(), *remap_collection_id)
2610 }
2611
2612 GenericSourceConnection::LoadGenerator(_) => continue,
2617 },
2618 _ => {
2620 continue;
2621 }
2622 };
2623
2624 let config = self.config().clone();
2626
2627 let read_handle = self.read_handle_for_snapshot(remap_id).await?;
2635
2636 let remap_read_hold = self
2639 .storage_collections
2640 .acquire_read_holds(vec![remap_id])
2641 .map_err(|_e| StorageError::ReadBeforeSince(remap_id))?
2642 .expect_element(|| "known to be exactly one");
2643
2644 let remap_as_of = remap_read_hold
2645 .since()
2646 .to_owned()
2647 .into_option()
2648 .ok_or(StorageError::ReadBeforeSince(remap_id))?;
2649
2650 rtr_futures.insert(
2651 id,
2652 tokio::time::timeout(timeout, async move {
2653 use mz_storage_types::sources::SourceConnection as _;
2654
2655 let as_of = Antichain::from_elem(remap_as_of);
2658 let remap_subscribe = read_handle
2659 .subscribe(as_of.clone())
2660 .await
2661 .map_err(|_| StorageError::ReadBeforeSince(remap_id))?;
2662
2663 tracing::debug!(?id, type_ = source_conn.name(), upstream = ?source_conn.external_reference(), "fetching real time recency");
2664
2665 let result = rtr::real_time_recency_ts(
2666 source_conn,
2667 id,
2668 config,
2669 as_of,
2670 remap_subscribe,
2671 )
2672 .await
2673 .map_err(|e| {
2674 tracing::debug!(?id, "real time recency error: {:?}", e);
2675 e
2676 });
2677
2678 drop(remap_read_hold);
2680
2681 result
2682 }),
2683 );
2684 }
2685
2686 Ok(Box::pin(async move {
2687 let (ids, futs): (Vec<_>, Vec<_>) = rtr_futures.into_iter().unzip();
2688 ids.into_iter()
2689 .zip_eq(futures::future::join_all(futs).await)
2690 .try_fold(Timestamp::MIN, |curr, (id, per_source_res)| {
2691 let new =
2692 per_source_res.map_err(|_e: Elapsed| StorageError::RtrTimeout(id))??;
2693 Ok::<_, StorageError>(std::cmp::max(curr, new))
2694 })
2695 }))
2696 }
2697
2698 fn dump(&self) -> Result<serde_json::Value, anyhow::Error> {
2699 let Self {
2701 build_info: _,
2702 now: _,
2703 read_only,
2704 collections,
2705 dropped_objects,
2706 persist_table_worker: _,
2707 txns_read: _,
2708 txns_metrics: _,
2709 stashed_responses,
2710 pending_table_handle_drops_tx: _,
2711 pending_table_handle_drops_rx: _,
2712 pending_oneshot_ingestions,
2713 collection_manager: _,
2714 introspection_ids,
2715 introspection_tokens: _,
2716 source_statistics: _,
2717 sink_statistics: _,
2718 statistics_interval_sender: _,
2719 instances,
2720 replica_dyncfg_overrides: _,
2721 initialized,
2722 config,
2723 persist_location,
2724 persist: _,
2725 metrics: _,
2726 recorded_frontiers,
2727 recorded_replica_frontiers,
2728 wallclock_lag: _,
2729 wallclock_lag_last_recorded,
2730 storage_collections: _,
2731 migrated_storage_collections,
2732 maintenance_ticker: _,
2733 maintenance_scheduled,
2734 instance_response_tx: _,
2735 instance_response_rx: _,
2736 persist_warm_task: _,
2737 } = self;
2738
2739 let collections: BTreeMap<_, _> = collections
2740 .iter()
2741 .map(|(id, c)| (id.to_string(), format!("{c:?}")))
2742 .collect();
2743 let dropped_objects: BTreeMap<_, _> = dropped_objects
2744 .iter()
2745 .map(|(id, rs)| (id.to_string(), format!("{rs:?}")))
2746 .collect();
2747 let stashed_responses: Vec<_> =
2748 stashed_responses.iter().map(|r| format!("{r:?}")).collect();
2749 let pending_oneshot_ingestions: BTreeMap<_, _> = pending_oneshot_ingestions
2750 .iter()
2751 .map(|(uuid, i)| (uuid.to_string(), format!("{i:?}")))
2752 .collect();
2753 let introspection_ids: BTreeMap<_, _> = introspection_ids
2754 .iter()
2755 .map(|(typ, id)| (format!("{typ:?}"), id.to_string()))
2756 .collect();
2757 let instances: BTreeMap<_, _> = instances
2758 .iter()
2759 .map(|(id, i)| (id.to_string(), format!("{i:?}")))
2760 .collect();
2761 let recorded_frontiers: BTreeMap<_, _> = recorded_frontiers
2762 .iter()
2763 .map(|(id, fs)| (id.to_string(), format!("{fs:?}")))
2764 .collect();
2765 let recorded_replica_frontiers: Vec<_> = recorded_replica_frontiers
2766 .iter()
2767 .map(|((gid, rid), f)| (gid.to_string(), rid.to_string(), format!("{f:?}")))
2768 .collect();
2769 let migrated_storage_collections: Vec<_> = migrated_storage_collections
2770 .iter()
2771 .map(|id| id.to_string())
2772 .collect();
2773
2774 Ok(serde_json::json!({
2775 "read_only": read_only,
2776 "collections": collections,
2777 "dropped_objects": dropped_objects,
2778 "stashed_responses": stashed_responses,
2779 "pending_oneshot_ingestions": pending_oneshot_ingestions,
2780 "introspection_ids": introspection_ids,
2781 "instances": instances,
2782 "initialized": initialized,
2783 "config": format!("{config:?}"),
2784 "persist_location": format!("{persist_location:?}"),
2785 "recorded_frontiers": recorded_frontiers,
2786 "recorded_replica_frontiers": recorded_replica_frontiers,
2787 "wallclock_lag_last_recorded": format!("{wallclock_lag_last_recorded:?}"),
2788 "migrated_storage_collections": migrated_storage_collections,
2789 "maintenance_scheduled": maintenance_scheduled,
2790 }))
2791 }
2792}
2793
2794pub fn prepare_initialization(txn: &mut dyn StorageTxn) -> Result<(), StorageError> {
2801 if txn.get_txn_wal_shard().is_none() {
2802 let txns_id = ShardId::new();
2803 txn.write_txn_wal_shard(txns_id)?;
2804 }
2805
2806 Ok(())
2807}
2808
2809impl Controller
2810where
2811 Self: StorageController,
2812{
2813 pub async fn new(
2821 build_info: &'static BuildInfo,
2822 persist_location: PersistLocation,
2823 persist_clients: Arc<PersistClientCache>,
2824 now: NowFn,
2825 wallclock_lag: WallclockLagFn<Timestamp>,
2826 txns_metrics: Arc<TxnMetrics>,
2827 read_only: bool,
2828 metrics_registry: &MetricsRegistry,
2829 controller_metrics: ControllerMetrics,
2830 connection_context: ConnectionContext,
2831 txn: &dyn StorageTxn,
2832 storage_collections: Arc<dyn StorageCollections + Send + Sync>,
2833 ) -> Self {
2834 let txns_client = persist_clients
2835 .open(persist_location.clone())
2836 .await
2837 .expect("location should be valid");
2838
2839 let persist_warm_task = warm_persist_state_in_background(
2840 txns_client.clone(),
2841 txn.get_collection_metadata().into_values(),
2842 );
2843 let persist_warm_task = Some(persist_warm_task.abort_on_drop());
2844
2845 let txns_id = txn
2849 .get_txn_wal_shard()
2850 .expect("must call prepare initialization before creating storage controller");
2851
2852 let persist_table_worker = if read_only {
2853 let txns_write = txns_client
2854 .open_writer(
2855 txns_id,
2856 Arc::new(TxnsCodecRow::desc()),
2857 Arc::new(UnitSchema),
2858 Diagnostics {
2859 shard_name: "txns".to_owned(),
2860 handle_purpose: "follow txns upper".to_owned(),
2861 },
2862 )
2863 .await
2864 .expect("txns schema shouldn't change");
2865 persist_handles::PersistTableWriteWorker::new_read_only_mode(
2866 txns_write,
2867 txns_client.clone(),
2868 )
2869 } else {
2870 let mut txns = TxnsHandle::open(
2871 Timestamp::MIN,
2872 txns_client.clone(),
2873 txns_client.dyncfgs().clone(),
2874 Arc::clone(&txns_metrics),
2875 txns_id,
2876 Opaque::encode(&PersistEpoch::default()),
2877 )
2878 .await;
2879 txns.upgrade_version().await;
2880 persist_handles::PersistTableWriteWorker::new_txns(txns, txns_client.clone())
2881 };
2882 let txns_read = TxnsRead::start::<TxnsCodecRow>(txns_client.clone(), txns_id).await;
2883
2884 let collection_manager = collection_mgmt::CollectionManager::new(read_only, now.clone());
2885
2886 let introspection_ids = BTreeMap::new();
2887 let introspection_tokens = Arc::new(Mutex::new(BTreeMap::new()));
2888
2889 let (statistics_interval_sender, _) =
2890 channel(mz_storage_types::parameters::STATISTICS_INTERVAL_DEFAULT);
2891
2892 let (pending_table_handle_drops_tx, pending_table_handle_drops_rx) =
2893 tokio::sync::mpsc::unbounded_channel();
2894
2895 let mut maintenance_ticker = tokio::time::interval(Duration::from_secs(1));
2896 maintenance_ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
2897
2898 let (instance_response_tx, instance_response_rx) = mpsc::unbounded_channel();
2899
2900 let metrics = StorageControllerMetrics::new(metrics_registry, controller_metrics);
2901
2902 let now_dt = mz_ore::now::to_datetime(now());
2903
2904 Self {
2905 build_info,
2906 collections: BTreeMap::default(),
2907 dropped_objects: Default::default(),
2908 persist_table_worker,
2909 txns_read,
2910 txns_metrics,
2911 stashed_responses: vec![],
2912 pending_table_handle_drops_tx,
2913 pending_table_handle_drops_rx,
2914 pending_oneshot_ingestions: BTreeMap::default(),
2915 collection_manager,
2916 introspection_ids,
2917 introspection_tokens,
2918 now,
2919 read_only,
2920 source_statistics: Arc::new(Mutex::new(statistics::SourceStatistics {
2921 source_statistics: BTreeMap::new(),
2922 webhook_statistics: BTreeMap::new(),
2923 })),
2924 sink_statistics: Arc::new(Mutex::new(BTreeMap::new())),
2925 statistics_interval_sender,
2926 instances: BTreeMap::new(),
2927 replica_dyncfg_overrides: BTreeMap::new(),
2928 initialized: false,
2929 config: StorageConfiguration::new(connection_context, mz_dyncfgs::all_dyncfgs()),
2930 persist_location,
2931 persist: persist_clients,
2932 metrics,
2933 recorded_frontiers: BTreeMap::new(),
2934 recorded_replica_frontiers: BTreeMap::new(),
2935 wallclock_lag,
2936 wallclock_lag_last_recorded: now_dt,
2937 storage_collections,
2938 migrated_storage_collections: BTreeSet::new(),
2939 maintenance_ticker,
2940 maintenance_scheduled: false,
2941 instance_response_rx,
2942 instance_response_tx,
2943 persist_warm_task,
2944 }
2945 }
2946
2947 #[instrument(level = "debug")]
2955 fn set_hold_policies(&mut self, policies: Vec<(GlobalId, ReadPolicy)>) {
2956 let mut read_capability_changes = BTreeMap::default();
2957
2958 for (id, policy) in policies.into_iter() {
2959 if let Some(collection) = self.collections.get_mut(&id) {
2960 let (write_frontier, derived_since, hold_policy) = match &mut collection.extra_state
2961 {
2962 CollectionStateExtra::Ingestion(ingestion) => (
2963 ingestion.write_frontier.borrow(),
2964 &mut ingestion.derived_since,
2965 &mut ingestion.hold_policy,
2966 ),
2967 CollectionStateExtra::None => {
2968 unreachable!("set_hold_policies is only called for ingestions");
2969 }
2970 CollectionStateExtra::Export(export) => (
2971 export.write_frontier.borrow(),
2972 &mut export.derived_since,
2973 &mut export.read_policy,
2974 ),
2975 };
2976
2977 let new_derived_since = policy.frontier(write_frontier);
2978 let mut update = swap_updates(derived_since, new_derived_since);
2979 if !update.is_empty() {
2980 read_capability_changes.insert(id, update);
2981 }
2982
2983 *hold_policy = policy;
2984 }
2985 }
2986
2987 if !read_capability_changes.is_empty() {
2988 self.update_hold_capabilities(&mut read_capability_changes);
2989 }
2990 }
2991
2992 #[instrument(level = "debug", fields(updates))]
2993 fn update_write_frontier(&mut self, id: GlobalId, new_upper: &Antichain<Timestamp>) {
2994 let mut read_capability_changes = BTreeMap::default();
2995
2996 if let Some(collection) = self.collections.get_mut(&id) {
2997 let (write_frontier, derived_since, hold_policy) = match &mut collection.extra_state {
2998 CollectionStateExtra::Ingestion(ingestion) => (
2999 &mut ingestion.write_frontier,
3000 &mut ingestion.derived_since,
3001 &ingestion.hold_policy,
3002 ),
3003 CollectionStateExtra::None => {
3004 if matches!(collection.data_source, DataSource::Progress) {
3005 } else {
3007 tracing::error!(
3008 ?collection,
3009 ?new_upper,
3010 "updated write frontier for collection which is not an ingestion"
3011 );
3012 }
3013 return;
3014 }
3015 CollectionStateExtra::Export(export) => (
3016 &mut export.write_frontier,
3017 &mut export.derived_since,
3018 &export.read_policy,
3019 ),
3020 };
3021
3022 if PartialOrder::less_than(write_frontier, new_upper) {
3023 write_frontier.clone_from(new_upper);
3024 }
3025
3026 let new_derived_since = hold_policy.frontier(write_frontier.borrow());
3027 let mut update = swap_updates(derived_since, new_derived_since);
3028 if !update.is_empty() {
3029 read_capability_changes.insert(id, update);
3030 }
3031 } else if self.dropped_objects.contains_key(&id) {
3032 } else {
3035 soft_panic_or_log!("spurious upper update for {id}: {new_upper:?}");
3036 }
3037
3038 if !read_capability_changes.is_empty() {
3039 self.update_hold_capabilities(&mut read_capability_changes);
3040 }
3041 }
3042
3043 #[instrument(level = "debug", fields(updates))]
3047 fn update_hold_capabilities(
3048 &mut self,
3049 updates: &mut BTreeMap<GlobalId, ChangeBatch<Timestamp>>,
3050 ) {
3051 let mut collections_net = BTreeMap::new();
3053
3054 while let Some(key) = updates.keys().rev().next().cloned() {
3059 let mut update = updates.remove(&key).unwrap();
3060
3061 if key.is_user() {
3062 debug!(id = %key, ?update, "update_hold_capability");
3063 }
3064
3065 if let Some(collection) = self.collections.get_mut(&key) {
3066 match &mut collection.extra_state {
3067 CollectionStateExtra::Ingestion(ingestion) => {
3068 let changes = ingestion.read_capabilities.update_iter(update.drain());
3069 update.extend(changes);
3070
3071 let (changes, frontier, _cluster_id) =
3072 collections_net.entry(key).or_insert_with(|| {
3073 (
3074 <ChangeBatch<_>>::new(),
3075 Antichain::new(),
3076 ingestion.instance_id,
3077 )
3078 });
3079
3080 changes.extend(update.drain());
3081 *frontier = ingestion.read_capabilities.frontier().to_owned();
3082 }
3083 CollectionStateExtra::None => {
3084 soft_panic_or_log!(
3086 "trying to update holds for collection {collection:?} which is not \
3087 an ingestion: {update:?}"
3088 );
3089 continue;
3090 }
3091 CollectionStateExtra::Export(export) => {
3092 let changes = export.read_capabilities.update_iter(update.drain());
3093 update.extend(changes);
3094
3095 let (changes, frontier, _cluster_id) =
3096 collections_net.entry(key).or_insert_with(|| {
3097 (<ChangeBatch<_>>::new(), Antichain::new(), export.cluster_id)
3098 });
3099
3100 changes.extend(update.drain());
3101 *frontier = export.read_capabilities.frontier().to_owned();
3102 }
3103 }
3104 } else {
3105 tracing::warn!(id = ?key, ?update, "update_hold_capabilities for unknown object");
3107 }
3108 }
3109
3110 for (key, (mut changes, frontier, cluster_id)) in collections_net {
3113 if !changes.is_empty() {
3114 if key.is_user() {
3115 debug!(id = %key, ?frontier, "downgrading ingestion read holds!");
3116 }
3117
3118 let collection = self
3119 .collections
3120 .get_mut(&key)
3121 .expect("missing collection state");
3122
3123 let read_holds = match &mut collection.extra_state {
3124 CollectionStateExtra::Ingestion(ingestion) => {
3125 ingestion.dependency_read_holds.as_mut_slice()
3126 }
3127 CollectionStateExtra::Export(export) => export.read_holds.as_mut_slice(),
3128 CollectionStateExtra::None => {
3129 soft_panic_or_log!(
3130 "trying to downgrade read holds for collection which is not an \
3131 ingestion: {collection:?}"
3132 );
3133 continue;
3134 }
3135 };
3136
3137 for read_hold in read_holds.iter_mut() {
3138 read_hold
3139 .try_downgrade(frontier.clone())
3140 .expect("we only advance the frontier");
3141 }
3142
3143 if let Some(instance) = self.instances.get_mut(&cluster_id) {
3145 instance.send(StorageCommand::AllowCompaction(key, frontier.clone()));
3146 } else {
3147 soft_panic_or_log!(
3148 "missing instance client for cluster {cluster_id} while we still have outstanding AllowCompaction command {frontier:?} for {key}"
3149 );
3150 }
3151 }
3152 }
3153 }
3154
3155 fn validate_collection_ids(
3157 &self,
3158 ids: impl Iterator<Item = GlobalId>,
3159 ) -> Result<(), StorageError> {
3160 for id in ids {
3161 self.storage_collections.check_exists(id)?;
3162 }
3163 Ok(())
3164 }
3165
3166 fn validate_export_ids(&self, ids: impl Iterator<Item = GlobalId>) -> Result<(), StorageError> {
3168 for id in ids {
3169 self.export(id)?;
3170 }
3171 Ok(())
3172 }
3173
3174 async fn open_data_handles(
3176 &self,
3177 id: &GlobalId,
3178 shard: ShardId,
3179 relation_desc: RelationDesc,
3180 persist_client: &PersistClient,
3181 ) -> WriteHandle<SourceData, (), Timestamp, StorageDiff> {
3182 let diagnostics = Diagnostics {
3183 shard_name: id.to_string(),
3184 handle_purpose: format!("controller data for {}", id),
3185 };
3186
3187 let mut write = persist_client
3188 .open_writer(
3189 shard,
3190 Arc::new(relation_desc),
3191 Arc::new(UnitSchema),
3192 diagnostics.clone(),
3193 )
3194 .await
3195 .expect("invalid persist usage");
3196
3197 write.fetch_recent_upper().await;
3199
3200 write
3201 }
3202
3203 fn register_introspection_collection(
3208 &mut self,
3209 id: GlobalId,
3210 introspection_type: IntrospectionType,
3211 write_handle: WriteHandle<SourceData, (), Timestamp, StorageDiff>,
3212 persist_client: PersistClient,
3213 ) -> Result<(), StorageError> {
3214 tracing::info!(%id, ?introspection_type, "registering introspection collection");
3215
3216 let force_writable = self.read_only && self.migrated_storage_collections.contains(&id);
3224 if force_writable {
3225 assert!(id.is_system(), "unexpected non-system global id: {id:?}");
3226 info!("writing to migrated storage collection {id} in read-only mode");
3227 }
3228
3229 let prev = self.introspection_ids.insert(introspection_type, id);
3230 assert!(
3231 prev.is_none(),
3232 "cannot have multiple IDs for introspection type"
3233 );
3234
3235 let metadata = self.storage_collections.collection_metadata(id)?.clone();
3236
3237 let read_handle_fn = move || {
3238 let persist_client = persist_client.clone();
3239 let metadata = metadata.clone();
3240
3241 let fut = async move {
3242 let read_handle = persist_client
3243 .open_leased_reader::<SourceData, (), Timestamp, StorageDiff>(
3244 metadata.data_shard,
3245 Arc::new(metadata.relation_desc.clone()),
3246 Arc::new(UnitSchema),
3247 Diagnostics {
3248 shard_name: id.to_string(),
3249 handle_purpose: format!("snapshot {}", id),
3250 },
3251 USE_CRITICAL_SINCE_SNAPSHOT.get(persist_client.dyncfgs()),
3252 )
3253 .await
3254 .expect("invalid persist usage");
3255 read_handle
3256 };
3257
3258 fut.boxed()
3259 };
3260
3261 let recent_upper = write_handle.shared_upper();
3262
3263 match CollectionManagerKind::from(&introspection_type) {
3264 CollectionManagerKind::Differential => {
3269 let statistics_retention_duration =
3270 dyncfgs::STATISTICS_RETENTION_DURATION.get(self.config().config_set());
3271
3272 let introspection_config = DifferentialIntrospectionConfig {
3274 recent_upper,
3275 introspection_type,
3276 storage_collections: Arc::clone(&self.storage_collections),
3277 collection_manager: self.collection_manager.clone(),
3278 source_statistics: Arc::clone(&self.source_statistics),
3279 sink_statistics: Arc::clone(&self.sink_statistics),
3280 statistics_interval: self.config.parameters.statistics_interval.clone(),
3281 statistics_interval_receiver: self.statistics_interval_sender.subscribe(),
3282 statistics_retention_duration,
3283 metrics: self.metrics.clone(),
3284 introspection_tokens: Arc::clone(&self.introspection_tokens),
3285 };
3286 self.collection_manager.register_differential_collection(
3287 id,
3288 write_handle,
3289 read_handle_fn,
3290 force_writable,
3291 introspection_config,
3292 );
3293 }
3294 CollectionManagerKind::AppendOnly => {
3302 let introspection_config = AppendOnlyIntrospectionConfig {
3303 introspection_type,
3304 config_set: Arc::clone(self.config.config_set()),
3305 parameters: self.config.parameters.clone(),
3306 storage_collections: Arc::clone(&self.storage_collections),
3307 };
3308 self.collection_manager.register_append_only_collection(
3309 id,
3310 write_handle,
3311 force_writable,
3312 Some(introspection_config),
3313 );
3314 }
3315 }
3316
3317 Ok(())
3318 }
3319
3320 fn reconcile_dangling_statistics(&self) {
3323 self.source_statistics
3324 .lock()
3325 .expect("poisoned")
3326 .source_statistics
3327 .retain(|(k, _replica_id), _| self.storage_collections.check_exists(*k).is_ok());
3329 self.sink_statistics
3330 .lock()
3331 .expect("poisoned")
3332 .retain(|(k, _replica_id), _| self.export(*k).is_ok());
3333 }
3334
3335 #[instrument(level = "debug")]
3345 fn append_shard_mappings<I>(&self, global_ids: I, diff: Diff)
3346 where
3347 I: Iterator<Item = GlobalId>,
3348 {
3349 mz_ore::soft_assert_or_log!(
3350 diff == Diff::MINUS_ONE || diff == Diff::ONE,
3351 "use 1 for insert or -1 for delete"
3352 );
3353
3354 let id = *self
3355 .introspection_ids
3356 .get(&IntrospectionType::ShardMapping)
3357 .expect("should be registered before this call");
3358
3359 let mut updates = vec![];
3360 let mut row_buf = Row::default();
3362
3363 for global_id in global_ids {
3364 let shard_id = if let Some(collection) = self.collections.get(&global_id) {
3365 collection.collection_metadata.data_shard.clone()
3366 } else {
3367 panic!("unknown global id: {}", global_id);
3368 };
3369
3370 let mut packer = row_buf.packer();
3371 packer.push(Datum::from(global_id.to_string().as_str()));
3372 packer.push(Datum::from(shard_id.to_string().as_str()));
3373 updates.push((row_buf.clone(), diff));
3374 }
3375
3376 self.collection_manager.differential_append(id, updates);
3377 }
3378
3379 fn determine_collection_dependencies(
3381 &self,
3382 self_id: GlobalId,
3383 collection_desc: &CollectionDescription,
3384 ) -> Result<Vec<GlobalId>, StorageError> {
3385 let mut dependencies = Vec::new();
3386
3387 if let Some(id) = collection_desc.primary {
3388 dependencies.push(id);
3389 }
3390
3391 match &collection_desc.data_source {
3392 DataSource::Introspection(_)
3393 | DataSource::Webhook
3394 | DataSource::Table
3395 | DataSource::Progress
3396 | DataSource::Other => (),
3397 DataSource::IngestionExport { ingestion_id, .. } => {
3398 let source_collection = self.collection(*ingestion_id)?;
3401 let ingestion_remap_collection_id = match &source_collection.data_source {
3402 DataSource::Ingestion(ingestion) => ingestion.remap_collection_id,
3403 _ => unreachable!(
3404 "SourceExport must only refer to primary sources that already exist"
3405 ),
3406 };
3407
3408 dependencies.extend([self_id, ingestion_remap_collection_id]);
3414 }
3415 DataSource::Ingestion(ingestion) => {
3417 dependencies.push(self_id);
3422 if self_id != ingestion.remap_collection_id {
3423 dependencies.push(ingestion.remap_collection_id);
3424 }
3425 }
3426 DataSource::Sink { desc } => {
3427 dependencies.extend([self_id, desc.sink.from]);
3429 }
3430 };
3431
3432 Ok(dependencies)
3433 }
3434
3435 async fn read_handle_for_snapshot(
3436 &self,
3437 id: GlobalId,
3438 ) -> Result<ReadHandle<SourceData, (), Timestamp, StorageDiff>, StorageError> {
3439 let metadata = self.storage_collections.collection_metadata(id)?;
3440 read_handle_for_snapshot(&self.persist, id, &metadata).await
3441 }
3442
3443 fn record_status_updates(&mut self, updates: Vec<StatusUpdate>) {
3446 if self.read_only {
3447 return;
3448 }
3449
3450 let mut sink_status_updates = vec![];
3451 let mut source_status_updates = vec![];
3452
3453 for update in updates {
3454 let id = update.id;
3455 if self.export(id).is_ok() {
3456 sink_status_updates.push(update);
3457 } else if self.storage_collections.check_exists(id).is_ok() {
3458 source_status_updates.push(update);
3459 }
3460 }
3461
3462 self.append_status_introspection_updates(
3463 IntrospectionType::SourceStatusHistory,
3464 source_status_updates,
3465 );
3466 self.append_status_introspection_updates(
3467 IntrospectionType::SinkStatusHistory,
3468 sink_status_updates,
3469 );
3470 }
3471
3472 fn collection(&self, id: GlobalId) -> Result<&CollectionState, StorageError> {
3473 self.collections
3474 .get(&id)
3475 .ok_or(StorageError::IdentifierMissing(id))
3476 }
3477
3478 fn run_ingestion(&mut self, id: GlobalId) -> Result<(), StorageError> {
3481 tracing::info!(%id, "starting ingestion");
3482
3483 let collection = self.collection(id)?;
3484 let ingestion_description = match &collection.data_source {
3485 DataSource::Ingestion(i) => i.clone(),
3486 _ => {
3487 tracing::warn!("run_ingestion called on non-ingestion ID {}", id);
3488 Err(StorageError::IdentifierInvalid(id))?
3489 }
3490 };
3491
3492 let mut source_exports = BTreeMap::new();
3494 for (export_id, export) in ingestion_description.source_exports.clone() {
3495 let export_storage_metadata = self.collection(export_id)?.collection_metadata.clone();
3496 source_exports.insert(
3497 export_id,
3498 SourceExport {
3499 storage_metadata: export_storage_metadata,
3500 details: export.details,
3501 data_config: export.data_config,
3502 },
3503 );
3504 }
3505
3506 let remap_collection = self.collection(ingestion_description.remap_collection_id)?;
3507
3508 let description = IngestionDescription::<CollectionMetadata> {
3509 source_exports,
3510 remap_metadata: remap_collection.collection_metadata.clone(),
3511 desc: ingestion_description.desc.clone(),
3513 instance_id: ingestion_description.instance_id,
3514 remap_collection_id: ingestion_description.remap_collection_id,
3515 };
3516
3517 let storage_instance_id = description.instance_id;
3518 let instance = self
3520 .instances
3521 .get_mut(&storage_instance_id)
3522 .ok_or_else(|| StorageError::IngestionInstanceMissing {
3523 storage_instance_id,
3524 ingestion_id: id,
3525 })?;
3526
3527 let augmented_ingestion = Box::new(RunIngestionCommand { id, description });
3528 instance.send(StorageCommand::RunIngestion(augmented_ingestion));
3529
3530 Ok(())
3531 }
3532
3533 fn run_export(&mut self, id: GlobalId) -> Result<(), StorageError> {
3536 let DataSource::Sink { desc: description } = &self.collections[&id].data_source else {
3537 return Err(StorageError::IdentifierMissing(id));
3538 };
3539
3540 let from_storage_metadata = self
3541 .storage_collections
3542 .collection_metadata(description.sink.from)?;
3543 let to_storage_metadata = self.storage_collections.collection_metadata(id)?;
3544
3545 let export_state = self.storage_collections.collection_frontiers(id)?;
3549 let mut as_of = description.sink.as_of.clone();
3550 as_of.join_assign(&export_state.implied_capability);
3551 let with_snapshot = description.sink.with_snapshot
3552 && !PartialOrder::less_than(&as_of, &export_state.write_frontier);
3553
3554 info!(
3555 sink_id = %id,
3556 from_id = %description.sink.from,
3557 write_frontier = ?export_state.write_frontier,
3558 ?as_of,
3559 ?with_snapshot,
3560 "run_export"
3561 );
3562
3563 let cmd = RunSinkCommand {
3564 id,
3565 description: StorageSinkDesc {
3566 from: description.sink.from,
3567 from_desc: description.sink.from_desc.clone(),
3568 connection: description.sink.connection.clone(),
3569 envelope: description.sink.envelope,
3570 as_of,
3571 version: description.sink.version,
3572 from_storage_metadata,
3573 with_snapshot,
3574 to_storage_metadata,
3575 commit_interval: description.sink.commit_interval,
3576 },
3577 };
3578
3579 let storage_instance_id = description.instance_id.clone();
3580
3581 let instance = self
3582 .instances
3583 .get_mut(&storage_instance_id)
3584 .ok_or_else(|| StorageError::ExportInstanceMissing {
3585 storage_instance_id,
3586 export_id: id,
3587 })?;
3588
3589 instance.send(StorageCommand::RunSink(Box::new(cmd)));
3590
3591 Ok(())
3592 }
3593
3594 fn update_frontier_introspection(&mut self) {
3599 let mut global_frontiers = BTreeMap::new();
3600 let mut replica_frontiers = BTreeMap::new();
3601
3602 for collection_frontiers in self.storage_collections.active_collection_frontiers() {
3603 let id = collection_frontiers.id;
3604 let since = collection_frontiers.read_capabilities;
3605 let upper = collection_frontiers.write_frontier;
3606
3607 let instance = self
3608 .collections
3609 .get(&id)
3610 .and_then(|collection_state| match &collection_state.extra_state {
3611 CollectionStateExtra::Ingestion(ingestion) => Some(ingestion.instance_id),
3612 CollectionStateExtra::Export(export) => Some(export.cluster_id()),
3613 CollectionStateExtra::None => None,
3614 })
3615 .and_then(|i| self.instances.get(&i));
3616
3617 if let Some(instance) = instance {
3618 for replica_id in instance.replica_ids() {
3619 replica_frontiers.insert((id, replica_id), upper.clone());
3620 }
3621 }
3622
3623 global_frontiers.insert(id, (since, upper));
3624 }
3625
3626 let mut global_updates = Vec::new();
3627 let mut replica_updates = Vec::new();
3628
3629 let mut push_global_update =
3630 |id: GlobalId,
3631 (since, upper): (Antichain<Timestamp>, Antichain<Timestamp>),
3632 diff: Diff| {
3633 let read_frontier = since.into_option().map_or(Datum::Null, |t| t.into());
3634 let write_frontier = upper.into_option().map_or(Datum::Null, |t| t.into());
3635 let row = Row::pack_slice(&[
3636 Datum::String(&id.to_string()),
3637 read_frontier,
3638 write_frontier,
3639 ]);
3640 global_updates.push((row, diff));
3641 };
3642
3643 let mut push_replica_update =
3644 |(id, replica_id): (GlobalId, ReplicaId), upper: Antichain<Timestamp>, diff: Diff| {
3645 let write_frontier = upper.into_option().map_or(Datum::Null, |t| t.into());
3646 let row = Row::pack_slice(&[
3647 Datum::String(&id.to_string()),
3648 Datum::String(&replica_id.to_string()),
3649 write_frontier,
3650 ]);
3651 replica_updates.push((row, diff));
3652 };
3653
3654 let mut old_global_frontiers =
3655 std::mem::replace(&mut self.recorded_frontiers, global_frontiers);
3656 for (&id, new) in &self.recorded_frontiers {
3657 match old_global_frontiers.remove(&id) {
3658 Some(old) if &old != new => {
3659 push_global_update(id, new.clone(), Diff::ONE);
3660 push_global_update(id, old, Diff::MINUS_ONE);
3661 }
3662 Some(_) => (),
3663 None => push_global_update(id, new.clone(), Diff::ONE),
3664 }
3665 }
3666 for (id, old) in old_global_frontiers {
3667 push_global_update(id, old, Diff::MINUS_ONE);
3668 }
3669
3670 let mut old_replica_frontiers =
3671 std::mem::replace(&mut self.recorded_replica_frontiers, replica_frontiers);
3672 for (&key, new) in &self.recorded_replica_frontiers {
3673 match old_replica_frontiers.remove(&key) {
3674 Some(old) if &old != new => {
3675 push_replica_update(key, new.clone(), Diff::ONE);
3676 push_replica_update(key, old, Diff::MINUS_ONE);
3677 }
3678 Some(_) => (),
3679 None => push_replica_update(key, new.clone(), Diff::ONE),
3680 }
3681 }
3682 for (key, old) in old_replica_frontiers {
3683 push_replica_update(key, old, Diff::MINUS_ONE);
3684 }
3685
3686 let id = self.introspection_ids[&IntrospectionType::Frontiers];
3687 self.collection_manager
3688 .differential_append(id, global_updates);
3689
3690 let id = self.introspection_ids[&IntrospectionType::ReplicaFrontiers];
3691 self.collection_manager
3692 .differential_append(id, replica_updates);
3693 }
3694
3695 fn refresh_wallclock_lag(&mut self) {
3714 let now_ms = (self.now)();
3715 let histogram_period =
3716 WallclockLagHistogramPeriod::from_epoch_millis(now_ms, self.config.config_set());
3717
3718 let frontier_lag = |frontier: &Antichain<Timestamp>| match frontier.as_option() {
3719 Some(ts) => (self.wallclock_lag)(*ts),
3720 None => Duration::ZERO,
3721 };
3722
3723 for frontiers in self.storage_collections.active_collection_frontiers() {
3724 let id = frontiers.id;
3725 let Some(collection) = self.collections.get_mut(&id) else {
3726 continue;
3727 };
3728
3729 let collection_unreadable =
3730 PartialOrder::less_equal(&frontiers.write_frontier, &frontiers.read_capabilities);
3731 let lag = if collection_unreadable {
3732 WallclockLag::Undefined
3733 } else {
3734 let lag = frontier_lag(&frontiers.write_frontier);
3735 WallclockLag::Seconds(lag.as_secs())
3736 };
3737
3738 collection.wallclock_lag_max = collection.wallclock_lag_max.max(lag);
3739
3740 let secs = lag.unwrap_seconds_or(u64::MAX);
3743 collection.wallclock_lag_metrics.observe(secs);
3744
3745 if let Some(stash) = &mut collection.wallclock_lag_histogram_stash {
3746 let bucket = lag.map_seconds(|secs| secs.next_power_of_two());
3747
3748 let instance_id = match &collection.extra_state {
3749 CollectionStateExtra::Ingestion(i) => Some(i.instance_id),
3750 CollectionStateExtra::Export(e) => Some(e.cluster_id()),
3751 CollectionStateExtra::None => None,
3752 };
3753 let workload_class = instance_id
3754 .and_then(|id| self.instances.get(&id))
3755 .and_then(|i| i.workload_class.clone());
3756 let labels = match workload_class {
3757 Some(wc) => [("workload_class", wc.clone())].into(),
3758 None => BTreeMap::new(),
3759 };
3760
3761 let key = (histogram_period, bucket, labels);
3762 *stash.entry(key).or_default() += Diff::ONE;
3763 }
3764 }
3765
3766 self.maybe_record_wallclock_lag();
3768 }
3769
3770 fn maybe_record_wallclock_lag(&mut self) {
3778 if self.read_only {
3779 return;
3780 }
3781
3782 let duration_trunc = |datetime: DateTime<_>, interval| {
3783 let td = TimeDelta::from_std(interval).ok()?;
3784 datetime.duration_trunc(td).ok()
3785 };
3786
3787 let interval = WALLCLOCK_LAG_RECORDING_INTERVAL.get(self.config.config_set());
3788 let now_dt = mz_ore::now::to_datetime((self.now)());
3789 let now_trunc = duration_trunc(now_dt, interval).unwrap_or_else(|| {
3790 soft_panic_or_log!("excessive wallclock lag recording interval: {interval:?}");
3791 let default = WALLCLOCK_LAG_RECORDING_INTERVAL.default();
3792 duration_trunc(now_dt, *default).unwrap()
3793 });
3794 if now_trunc <= self.wallclock_lag_last_recorded {
3795 return;
3796 }
3797
3798 let now_ts: CheckedTimestamp<_> = now_trunc.try_into().expect("must fit");
3799
3800 let mut history_updates = Vec::new();
3801 let mut histogram_updates = Vec::new();
3802 let mut row_buf = Row::default();
3803 for frontiers in self.storage_collections.active_collection_frontiers() {
3804 let id = frontiers.id;
3805 let Some(collection) = self.collections.get_mut(&id) else {
3806 continue;
3807 };
3808
3809 let max_lag = std::mem::replace(&mut collection.wallclock_lag_max, WallclockLag::MIN);
3810 let row = Row::pack_slice(&[
3811 Datum::String(&id.to_string()),
3812 Datum::Null,
3813 max_lag.into_interval_datum(),
3814 Datum::TimestampTz(now_ts),
3815 ]);
3816 history_updates.push((row, Diff::ONE));
3817
3818 let Some(stash) = &mut collection.wallclock_lag_histogram_stash else {
3819 continue;
3820 };
3821
3822 for ((period, lag, labels), count) in std::mem::take(stash) {
3823 let mut packer = row_buf.packer();
3824 packer.extend([
3825 Datum::TimestampTz(period.start),
3826 Datum::TimestampTz(period.end),
3827 Datum::String(&id.to_string()),
3828 lag.into_uint64_datum(),
3829 ]);
3830 let labels = labels.iter().map(|(k, v)| (*k, Datum::String(v)));
3831 packer.push_dict(labels);
3832
3833 histogram_updates.push((row_buf.clone(), count));
3834 }
3835 }
3836
3837 if !history_updates.is_empty() {
3838 self.append_introspection_updates(
3839 IntrospectionType::WallclockLagHistory,
3840 history_updates,
3841 );
3842 }
3843 if !histogram_updates.is_empty() {
3844 self.append_introspection_updates(
3845 IntrospectionType::WallclockLagHistogram,
3846 histogram_updates,
3847 );
3848 }
3849
3850 self.wallclock_lag_last_recorded = now_trunc;
3851 }
3852
3853 fn maintain(&mut self) {
3858 self.update_frontier_introspection();
3859 self.refresh_wallclock_lag();
3860
3861 for instance in self.instances.values_mut() {
3863 instance.refresh_state_metrics();
3864 }
3865 }
3866}
3867
3868impl From<&IntrospectionType> for CollectionManagerKind {
3869 fn from(value: &IntrospectionType) -> Self {
3870 match value {
3871 IntrospectionType::ShardMapping
3872 | IntrospectionType::Frontiers
3873 | IntrospectionType::ReplicaFrontiers
3874 | IntrospectionType::StorageSourceStatistics
3875 | IntrospectionType::StorageSinkStatistics
3876 | IntrospectionType::ComputeDependencies
3877 | IntrospectionType::ComputeOperatorHydrationStatus
3878 | IntrospectionType::ComputeMaterializedViewRefreshes
3879 | IntrospectionType::ComputeErrorCounts
3880 | IntrospectionType::ComputeHydrationTimes
3881 | IntrospectionType::ComputeObjectArrangementSizes => {
3882 CollectionManagerKind::Differential
3883 }
3884
3885 IntrospectionType::SourceStatusHistory
3886 | IntrospectionType::SinkStatusHistory
3887 | IntrospectionType::PrivatelinkConnectionStatusHistory
3888 | IntrospectionType::ReplicaStatusHistory
3889 | IntrospectionType::ReplicaMetricsHistory
3890 | IntrospectionType::WallclockLagHistory
3891 | IntrospectionType::WallclockLagHistogram
3892 | IntrospectionType::PreparedStatementHistory
3893 | IntrospectionType::StatementExecutionHistory
3894 | IntrospectionType::SessionHistory
3895 | IntrospectionType::StatementLifecycleHistory
3896 | IntrospectionType::SqlText => CollectionManagerKind::AppendOnly,
3897 }
3898 }
3899}
3900
3901async fn snapshot_statistics(
3907 id: GlobalId,
3908 upper: Antichain<Timestamp>,
3909 storage_collections: &Arc<dyn StorageCollections + Send + Sync>,
3910) -> Vec<Row> {
3911 match upper.as_option() {
3912 Some(f) if f > &Timestamp::MIN => {
3913 let as_of = f.step_back().unwrap();
3914
3915 let snapshot = storage_collections.snapshot(id, as_of).await.unwrap();
3916 snapshot
3917 .into_iter()
3918 .map(|(row, diff)| {
3919 assert_eq!(diff, 1);
3920 row
3921 })
3922 .collect()
3923 }
3924 _ => Vec::new(),
3927 }
3928}
3929
3930async fn read_handle_for_snapshot(
3931 persist: &PersistClientCache,
3932 id: GlobalId,
3933 metadata: &CollectionMetadata,
3934) -> Result<ReadHandle<SourceData, (), Timestamp, StorageDiff>, StorageError> {
3935 let persist_client = persist
3936 .open(metadata.persist_location.clone())
3937 .await
3938 .unwrap();
3939
3940 let read_handle = persist_client
3945 .open_leased_reader::<SourceData, (), _, _>(
3946 metadata.data_shard,
3947 Arc::new(metadata.relation_desc.clone()),
3948 Arc::new(UnitSchema),
3949 Diagnostics {
3950 shard_name: id.to_string(),
3951 handle_purpose: format!("snapshot {}", id),
3952 },
3953 USE_CRITICAL_SINCE_SNAPSHOT.get(persist_client.dyncfgs()),
3954 )
3955 .await
3956 .expect("invalid persist usage");
3957 Ok(read_handle)
3958}
3959
3960#[derive(Debug)]
3962struct CollectionState {
3963 pub data_source: DataSource,
3965
3966 pub collection_metadata: CollectionMetadata,
3967
3968 pub extra_state: CollectionStateExtra,
3969
3970 wallclock_lag_max: WallclockLag,
3972 wallclock_lag_histogram_stash: Option<
3979 BTreeMap<
3980 (
3981 WallclockLagHistogramPeriod,
3982 WallclockLag,
3983 BTreeMap<&'static str, String>,
3984 ),
3985 Diff,
3986 >,
3987 >,
3988 wallclock_lag_metrics: WallclockLagMetrics,
3990}
3991
3992impl CollectionState {
3993 fn new(
3994 data_source: DataSource,
3995 collection_metadata: CollectionMetadata,
3996 extra_state: CollectionStateExtra,
3997 wallclock_lag_metrics: WallclockLagMetrics,
3998 ) -> Self {
3999 let wallclock_lag_histogram_stash = match &data_source {
4003 DataSource::Other => None,
4004 _ => Some(Default::default()),
4005 };
4006
4007 Self {
4008 data_source,
4009 collection_metadata,
4010 extra_state,
4011 wallclock_lag_max: WallclockLag::MIN,
4012 wallclock_lag_histogram_stash,
4013 wallclock_lag_metrics,
4014 }
4015 }
4016}
4017
4018#[derive(Debug)]
4020enum CollectionStateExtra {
4021 Ingestion(IngestionState),
4022 Export(ExportState),
4023 None,
4024}
4025
4026#[derive(Debug)]
4028struct IngestionState {
4029 pub read_capabilities: MutableAntichain<Timestamp>,
4031
4032 pub derived_since: Antichain<Timestamp>,
4035
4036 pub dependency_read_holds: Vec<ReadHold>,
4038
4039 pub write_frontier: Antichain<Timestamp>,
4041
4042 pub hold_policy: ReadPolicy,
4049
4050 pub instance_id: StorageInstanceId,
4052
4053 pub hydrated_on: BTreeSet<ReplicaId>,
4055}
4056
4057struct StatusHistoryDesc<K> {
4062 retention_policy: StatusHistoryRetentionPolicy,
4063 extract_key: Box<dyn Fn(&[Datum]) -> K + Send>,
4064 extract_time: Box<dyn Fn(&[Datum]) -> CheckedTimestamp<DateTime<Utc>> + Send>,
4065}
4066enum StatusHistoryRetentionPolicy {
4067 LastN(usize),
4069 TimeWindow(Duration),
4071}
4072
4073fn source_status_history_desc(
4074 params: &StorageParameters,
4075) -> StatusHistoryDesc<(GlobalId, Option<ReplicaId>)> {
4076 let desc = &MZ_SOURCE_STATUS_HISTORY_DESC;
4077 let (source_id_idx, _) = desc.get_by_name(&"source_id".into()).expect("exists");
4078 let (replica_id_idx, _) = desc.get_by_name(&"replica_id".into()).expect("exists");
4079 let (time_idx, _) = desc.get_by_name(&"occurred_at".into()).expect("exists");
4080
4081 StatusHistoryDesc {
4082 retention_policy: StatusHistoryRetentionPolicy::LastN(
4083 params.keep_n_source_status_history_entries,
4084 ),
4085 extract_key: Box::new(move |datums| {
4086 (
4087 GlobalId::from_str(datums[source_id_idx].unwrap_str()).expect("GlobalId column"),
4088 if datums[replica_id_idx].is_null() {
4089 None
4090 } else {
4091 Some(
4092 ReplicaId::from_str(datums[replica_id_idx].unwrap_str())
4093 .expect("ReplicaId column"),
4094 )
4095 },
4096 )
4097 }),
4098 extract_time: Box::new(move |datums| datums[time_idx].unwrap_timestamptz()),
4099 }
4100}
4101
4102fn sink_status_history_desc(
4103 params: &StorageParameters,
4104) -> StatusHistoryDesc<(GlobalId, Option<ReplicaId>)> {
4105 let desc = &MZ_SINK_STATUS_HISTORY_DESC;
4106 let (sink_id_idx, _) = desc.get_by_name(&"sink_id".into()).expect("exists");
4107 let (replica_id_idx, _) = desc.get_by_name(&"replica_id".into()).expect("exists");
4108 let (time_idx, _) = desc.get_by_name(&"occurred_at".into()).expect("exists");
4109
4110 StatusHistoryDesc {
4111 retention_policy: StatusHistoryRetentionPolicy::LastN(
4112 params.keep_n_sink_status_history_entries,
4113 ),
4114 extract_key: Box::new(move |datums| {
4115 (
4116 GlobalId::from_str(datums[sink_id_idx].unwrap_str()).expect("GlobalId column"),
4117 if datums[replica_id_idx].is_null() {
4118 None
4119 } else {
4120 Some(
4121 ReplicaId::from_str(datums[replica_id_idx].unwrap_str())
4122 .expect("ReplicaId column"),
4123 )
4124 },
4125 )
4126 }),
4127 extract_time: Box::new(move |datums| datums[time_idx].unwrap_timestamptz()),
4128 }
4129}
4130
4131fn privatelink_status_history_desc(params: &StorageParameters) -> StatusHistoryDesc<GlobalId> {
4132 let desc = &MZ_AWS_PRIVATELINK_CONNECTION_STATUS_HISTORY_DESC;
4133 let (key_idx, _) = desc.get_by_name(&"connection_id".into()).expect("exists");
4134 let (time_idx, _) = desc.get_by_name(&"occurred_at".into()).expect("exists");
4135
4136 StatusHistoryDesc {
4137 retention_policy: StatusHistoryRetentionPolicy::LastN(
4138 params.keep_n_privatelink_status_history_entries,
4139 ),
4140 extract_key: Box::new(move |datums| {
4141 GlobalId::from_str(datums[key_idx].unwrap_str()).expect("GlobalId column")
4142 }),
4143 extract_time: Box::new(move |datums| datums[time_idx].unwrap_timestamptz()),
4144 }
4145}
4146
4147fn replica_status_history_desc(params: &StorageParameters) -> StatusHistoryDesc<(GlobalId, u64)> {
4148 let desc = &REPLICA_STATUS_HISTORY_DESC;
4149 let (replica_idx, _) = desc.get_by_name(&"replica_id".into()).expect("exists");
4150 let (process_idx, _) = desc.get_by_name(&"process_id".into()).expect("exists");
4151 let (time_idx, _) = desc.get_by_name(&"occurred_at".into()).expect("exists");
4152
4153 StatusHistoryDesc {
4154 retention_policy: StatusHistoryRetentionPolicy::TimeWindow(
4155 params.replica_status_history_retention_window,
4156 ),
4157 extract_key: Box::new(move |datums| {
4158 (
4159 GlobalId::from_str(datums[replica_idx].unwrap_str()).expect("GlobalId column"),
4160 datums[process_idx].unwrap_uint64(),
4161 )
4162 }),
4163 extract_time: Box::new(move |datums| datums[time_idx].unwrap_timestamptz()),
4164 }
4165}
4166
4167fn swap_updates(
4169 from: &mut Antichain<Timestamp>,
4170 mut replace_with: Antichain<Timestamp>,
4171) -> ChangeBatch<Timestamp> {
4172 let mut update = ChangeBatch::new();
4173 if PartialOrder::less_equal(from, &replace_with) {
4174 update.extend(replace_with.iter().map(|time| (*time, 1)));
4175 std::mem::swap(from, &mut replace_with);
4176 update.extend(replace_with.iter().map(|time| (*time, -1)));
4177 }
4178 update
4179}