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, restart_in_place, 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, shard_has_no_updates};
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 let reason = format!(
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, dependency_read_holds,
1036 );
1037 if self.read_only {
1043 restart_in_place!("{}", reason);
1044 } else {
1045 halt!("{}", reason);
1046 }
1047 }
1048
1049 if description.primary.is_none()
1070 && write_frontier.elements() != &[Timestamp::MIN]
1071 && !write_frontier.is_empty()
1072 && !PartialOrder::less_than(&dependency_since, &write_frontier)
1073 && !(matches!(data_source, DataSource::IngestionExport { .. })
1074 && shard_has_no_updates(persist_client, id, &metadata).await)
1075 {
1076 mz_ore::soft_panic_or_log!(
1077 "dependency since has advanced past dependent ({id}) upper \n
1078 dependent ({id}): upper {:?} \n
1079 dependency since {:?} \n
1080 dependency read holds: {:?}",
1081 write_frontier,
1082 dependency_since,
1083 dependency_read_holds,
1084 );
1085 }
1086 }
1087
1088 let mut extra_state = CollectionStateExtra::None;
1090 let mut maybe_instance_id = None;
1091 match &data_source {
1092 DataSource::Introspection(typ) => {
1093 debug!(
1094 ?data_source, meta = ?metadata,
1095 "registering {id} with persist monotonic worker",
1096 );
1097 self.register_introspection_collection(
1103 id,
1104 *typ,
1105 write.expect_handle("introspection collections are not tables"),
1106 persist_client.clone(),
1107 )?;
1108 }
1109 DataSource::Webhook => {
1110 debug!(
1111 ?data_source, meta = ?metadata,
1112 "registering {id} with persist monotonic worker",
1113 );
1114 new_webhook_statistic_entries.insert(id);
1117 self.collection_manager.register_append_only_collection(
1123 id,
1124 write.expect_handle("webhook collections are not tables"),
1125 false,
1126 None,
1127 );
1128 }
1129 DataSource::IngestionExport {
1130 ingestion_id,
1131 details,
1132 data_config,
1133 } => {
1134 debug!(
1135 ?data_source, meta = ?metadata,
1136 "not registering {id} with a controller persist worker",
1137 );
1138 let ingestion_state = self
1140 .collections
1141 .get_mut(ingestion_id)
1142 .expect("known to exist");
1143
1144 let instance_id = match &mut ingestion_state.data_source {
1145 DataSource::Ingestion(ingestion_desc) => {
1146 ingestion_desc.source_exports.insert(
1147 id,
1148 SourceExport {
1149 storage_metadata: (),
1150 details: details.clone(),
1151 data_config: data_config.clone(),
1152 },
1153 );
1154
1155 ingestion_desc.instance_id
1160 }
1161 _ => unreachable!(
1162 "SourceExport must only refer to primary sources that already exist"
1163 ),
1164 };
1165
1166 to_execute.remove(&id);
1168 to_execute.insert(*ingestion_id);
1169
1170 let ingestion_state = IngestionState {
1171 read_capabilities: MutableAntichain::from(dependency_since.clone()),
1172 dependency_read_holds,
1173 derived_since: dependency_since,
1174 write_frontier: Antichain::from_elem(Timestamp::MIN),
1175 hold_policy: ReadPolicy::step_back(),
1176 instance_id,
1177 hydrated_on: BTreeSet::new(),
1178 };
1179
1180 extra_state = CollectionStateExtra::Ingestion(ingestion_state);
1181 maybe_instance_id = Some(instance_id);
1182 }
1183 DataSource::Table => {
1184 debug!(
1185 ?data_source, meta = ?metadata,
1186 "not registering {id} with the txns shard here; the caller does that \
1187 through the group committer",
1188 );
1189 }
1190 DataSource::Progress | DataSource::Other => {
1191 debug!(
1192 ?data_source, meta = ?metadata,
1193 "not registering {id} with a controller persist worker",
1194 );
1195 }
1196 DataSource::Ingestion(ingestion_desc) => {
1197 debug!(
1198 ?data_source, meta = ?metadata,
1199 "not registering {id} with a controller persist worker",
1200 );
1201
1202 let mut dependency_since = Antichain::from_elem(Timestamp::MIN);
1203 for read_hold in dependency_read_holds.iter() {
1204 dependency_since.join_assign(read_hold.since());
1205 }
1206
1207 let ingestion_state = IngestionState {
1208 read_capabilities: MutableAntichain::from(dependency_since.clone()),
1209 dependency_read_holds,
1210 derived_since: dependency_since,
1211 write_frontier: Antichain::from_elem(Timestamp::MIN),
1212 hold_policy: ReadPolicy::step_back(),
1213 instance_id: ingestion_desc.instance_id,
1214 hydrated_on: BTreeSet::new(),
1215 };
1216
1217 extra_state = CollectionStateExtra::Ingestion(ingestion_state);
1218 maybe_instance_id = Some(ingestion_desc.instance_id);
1219 }
1220 DataSource::Sink { desc } => {
1221 let mut dependency_since = Antichain::from_elem(Timestamp::MIN);
1222 for read_hold in dependency_read_holds.iter() {
1223 dependency_since.join_assign(read_hold.since());
1224 }
1225
1226 let [self_hold, read_hold] =
1227 dependency_read_holds.try_into().expect("two holds");
1228
1229 let state = ExportState::new(
1230 desc.instance_id,
1231 read_hold,
1232 self_hold,
1233 write_frontier.clone(),
1234 ReadPolicy::step_back(),
1235 );
1236 maybe_instance_id = Some(state.cluster_id);
1237 extra_state = CollectionStateExtra::Export(state);
1238 }
1239 }
1240
1241 let wallclock_lag_metrics = self.metrics.wallclock_lag_metrics(id, maybe_instance_id);
1242 let collection_state =
1243 CollectionState::new(data_source, metadata, extra_state, wallclock_lag_metrics);
1244
1245 self.collections.insert(id, collection_state);
1246 }
1247
1248 {
1249 let mut source_statistics = self.source_statistics.lock().expect("poisoned");
1250
1251 for id in new_webhook_statistic_entries {
1254 source_statistics.webhook_statistics.entry(id).or_default();
1255 }
1256
1257 }
1261
1262 self.append_shard_mappings(new_collections.into_iter(), Diff::ONE);
1263
1264 for id in to_execute {
1266 match &self.collection(id)?.data_source {
1267 DataSource::Ingestion(ingestion) => {
1268 if !self.read_only
1269 || (ENABLE_0DT_DEPLOYMENT_SOURCES.get(self.config.config_set())
1270 && ingestion.desc.connection.supports_read_only())
1271 {
1272 self.run_ingestion(id)?;
1273 }
1274 }
1275 DataSource::IngestionExport { .. } => unreachable!(
1276 "ingestion exports do not execute directly, but instead schedule their source to be re-executed"
1277 ),
1278 DataSource::Introspection(_)
1279 | DataSource::Webhook
1280 | DataSource::Table
1281 | DataSource::Progress
1282 | DataSource::Other => {}
1283 DataSource::Sink { .. } => {
1284 if !self.read_only {
1285 self.run_export(id)?;
1286 }
1287 }
1288 };
1289 }
1290
1291 Ok(())
1292 }
1293
1294 fn check_alter_ingestion_source_desc(
1295 &mut self,
1296 ingestion_id: GlobalId,
1297 source_desc: &SourceDesc,
1298 ) -> Result<(), StorageError> {
1299 let source_collection = self.collection(ingestion_id)?;
1300 let data_source = &source_collection.data_source;
1301 match &data_source {
1302 DataSource::Ingestion(cur_ingestion) => {
1303 cur_ingestion
1304 .desc
1305 .alter_compatible(ingestion_id, source_desc)?;
1306 }
1307 o => {
1308 tracing::info!(
1309 "{ingestion_id} inalterable because its data source is {:?} and not an ingestion",
1310 o
1311 );
1312 Err(AlterError { id: ingestion_id })?
1313 }
1314 }
1315
1316 Ok(())
1317 }
1318
1319 async fn alter_ingestion_source_desc(
1320 &mut self,
1321 ingestion_ids: BTreeMap<GlobalId, SourceDesc>,
1322 ) -> Result<(), StorageError> {
1323 let mut ingestions_to_run = BTreeSet::new();
1324
1325 for (id, new_desc) in ingestion_ids {
1326 let collection = self
1327 .collections
1328 .get_mut(&id)
1329 .ok_or_else(|| StorageError::IdentifierMissing(id))?;
1330
1331 match &mut collection.data_source {
1332 DataSource::Ingestion(ingestion) => {
1333 if ingestion.desc != new_desc {
1334 tracing::info!(
1335 from = ?ingestion.desc,
1336 to = ?new_desc,
1337 "alter_ingestion_source_desc, updating"
1338 );
1339 ingestion.desc = new_desc;
1340 ingestions_to_run.insert(id);
1341 }
1342 }
1343 o => {
1344 tracing::warn!("alter_ingestion_source_desc called on {:?}", o);
1345 Err(StorageError::IdentifierInvalid(id))?;
1346 }
1347 }
1348 }
1349
1350 for id in ingestions_to_run {
1351 self.run_ingestion(id)?;
1352 }
1353 Ok(())
1354 }
1355
1356 async fn alter_ingestion_connections(
1357 &mut self,
1358 source_connections: BTreeMap<GlobalId, GenericSourceConnection<InlinedConnection>>,
1359 ) -> Result<(), StorageError> {
1360 let mut ingestions_to_run = BTreeSet::new();
1361
1362 for (id, conn) in source_connections {
1363 let collection = self
1364 .collections
1365 .get_mut(&id)
1366 .ok_or_else(|| StorageError::IdentifierMissing(id))?;
1367
1368 match &mut collection.data_source {
1369 DataSource::Ingestion(ingestion) => {
1370 if ingestion.desc.connection != conn {
1373 tracing::info!(from = ?ingestion.desc.connection, to = ?conn, "alter_ingestion_connections, updating");
1374 ingestion.desc.connection = conn;
1375 ingestions_to_run.insert(id);
1376 } else {
1377 tracing::warn!(
1378 "update_source_connection called on {id} but the \
1379 connection was the same"
1380 );
1381 }
1382 }
1383 o => {
1384 tracing::warn!("update_source_connection called on {:?}", o);
1385 Err(StorageError::IdentifierInvalid(id))?;
1386 }
1387 }
1388 }
1389
1390 for id in ingestions_to_run {
1391 self.run_ingestion(id)?;
1392 }
1393 Ok(())
1394 }
1395
1396 async fn alter_ingestion_export_data_configs(
1397 &mut self,
1398 source_exports: BTreeMap<GlobalId, SourceExportDataConfig>,
1399 ) -> Result<(), StorageError> {
1400 let mut ingestions_to_run = BTreeSet::new();
1401
1402 for (source_export_id, new_data_config) in source_exports {
1403 let source_export_collection = self
1406 .collections
1407 .get_mut(&source_export_id)
1408 .ok_or_else(|| StorageError::IdentifierMissing(source_export_id))?;
1409 let ingestion_id = match &mut source_export_collection.data_source {
1410 DataSource::IngestionExport {
1411 ingestion_id,
1412 details: _,
1413 data_config,
1414 } => {
1415 *data_config = new_data_config.clone();
1416 *ingestion_id
1417 }
1418 o => {
1419 tracing::warn!("alter_ingestion_export_data_configs called on {:?}", o);
1420 Err(StorageError::IdentifierInvalid(source_export_id))?
1421 }
1422 };
1423 let ingestion_collection = self
1426 .collections
1427 .get_mut(&ingestion_id)
1428 .ok_or_else(|| StorageError::IdentifierMissing(ingestion_id))?;
1429
1430 match &mut ingestion_collection.data_source {
1431 DataSource::Ingestion(ingestion_desc) => {
1432 let source_export = ingestion_desc
1433 .source_exports
1434 .get_mut(&source_export_id)
1435 .ok_or_else(|| StorageError::IdentifierMissing(source_export_id))?;
1436
1437 if source_export.data_config != new_data_config {
1440 tracing::info!(?source_export_id, from = ?source_export.data_config, to = ?new_data_config, "alter_ingestion_export_data_configs, updating");
1441 source_export.data_config = new_data_config;
1442
1443 ingestions_to_run.insert(ingestion_id);
1444 } else {
1445 tracing::warn!(
1446 "alter_ingestion_export_data_configs called on \
1447 export {source_export_id} of {ingestion_id} but \
1448 the data config was the same"
1449 );
1450 }
1451 }
1452 o => {
1453 tracing::warn!("alter_ingestion_export_data_configs called on {:?}", o);
1454 Err(StorageError::IdentifierInvalid(ingestion_id))?
1455 }
1456 }
1457 }
1458
1459 for id in ingestions_to_run {
1460 self.run_ingestion(id)?;
1461 }
1462 Ok(())
1463 }
1464
1465 async fn alter_table_desc(
1466 &mut self,
1467 existing_collection: GlobalId,
1468 new_collection: GlobalId,
1469 new_desc: RelationDesc,
1470 expected_version: RelationVersion,
1471 ) -> Result<(), StorageError> {
1472 let data_shard = {
1473 let Controller {
1474 collections,
1475 storage_collections,
1476 ..
1477 } = self;
1478
1479 let existing = collections
1480 .get(&existing_collection)
1481 .ok_or(StorageError::IdentifierMissing(existing_collection))?;
1482 if existing.data_source != DataSource::Table {
1483 return Err(StorageError::IdentifierInvalid(existing_collection));
1484 }
1485
1486 storage_collections
1488 .alter_table_desc(
1489 existing_collection,
1490 new_collection,
1491 new_desc.clone(),
1492 expected_version,
1493 )
1494 .await?;
1495
1496 existing.collection_metadata.data_shard.clone()
1497 };
1498
1499 let collection_meta = CollectionMetadata {
1500 persist_location: self.persist_location.clone(),
1501 data_shard,
1502 relation_desc: new_desc.clone(),
1503 txns_shard: Some(self.txns_read.txns_id().clone()),
1505 };
1506 let wallclock_lag_metrics = self.metrics.wallclock_lag_metrics(new_collection, None);
1508 let collection_state = CollectionState::new(
1509 DataSource::Table,
1510 collection_meta,
1511 CollectionStateExtra::None,
1512 wallclock_lag_metrics,
1513 );
1514
1515 self.collections.insert(new_collection, collection_state);
1518
1519 self.append_shard_mappings([new_collection].into_iter(), Diff::ONE);
1520
1521 Ok(())
1522 }
1523
1524 async fn register_table_collections(
1525 &mut self,
1526 register_ts: Timestamp,
1527 ids: Vec<GlobalId>,
1528 ) -> Result<(), StorageError> {
1529 let mut tables = self.table_registrations(ids)?;
1530
1531 if self.read_only {
1533 tables.retain(|table| self.migrated_storage_collections.contains(&table.id));
1534 }
1535 if tables.is_empty() {
1536 return Ok(());
1537 }
1538
1539 match self
1540 .persist_table_worker
1541 .register(register_ts, tables)
1542 .await
1543 {
1544 Ok(res) => res,
1545 Err(_recv) => Err(StorageError::ShuttingDown("persist_table_worker")),
1546 }
1547 }
1548
1549 fn table_registrations(
1550 &self,
1551 ids: Vec<GlobalId>,
1552 ) -> Result<Vec<TableRegistration>, StorageError> {
1553 let mut tables = Vec::with_capacity(ids.len());
1555 for id in ids {
1556 let collection = self.collection(id)?;
1557 if matches!(collection.data_source, DataSource::Table) {
1558 let metadata = &collection.collection_metadata;
1559 tables.push(TableRegistration {
1560 id,
1561 data_shard: metadata.data_shard,
1562 relation_desc: metadata.relation_desc.clone(),
1563 });
1564 }
1565 }
1566 Ok(tables)
1567 }
1568
1569 fn txns_table_ids(&self, ids: Vec<GlobalId>) -> Result<Vec<GlobalId>, StorageError> {
1570 let mut tables = Vec::with_capacity(ids.len());
1571 for id in ids {
1572 let collection = self.collection(id)?;
1573 if matches!(collection.data_source, DataSource::Table) {
1574 tables.push(id);
1575 }
1576 }
1577 Ok(tables)
1578 }
1579
1580 fn export(&self, id: GlobalId) -> Result<&ExportState, StorageError> {
1581 self.collections
1582 .get(&id)
1583 .and_then(|c| match &c.extra_state {
1584 CollectionStateExtra::Export(state) => Some(state),
1585 _ => None,
1586 })
1587 .ok_or(StorageError::IdentifierMissing(id))
1588 }
1589
1590 fn export_mut(&mut self, id: GlobalId) -> Result<&mut ExportState, StorageError> {
1591 self.collections
1592 .get_mut(&id)
1593 .and_then(|c| match &mut c.extra_state {
1594 CollectionStateExtra::Export(state) => Some(state),
1595 _ => None,
1596 })
1597 .ok_or(StorageError::IdentifierMissing(id))
1598 }
1599
1600 async fn create_oneshot_ingestion(
1602 &mut self,
1603 ingestion_id: uuid::Uuid,
1604 collection_id: GlobalId,
1605 instance_id: StorageInstanceId,
1606 request: OneshotIngestionRequest,
1607 result_tx: OneshotResultCallback<ProtoBatch>,
1608 ) -> Result<(), StorageError> {
1609 let collection_meta = self
1610 .collections
1611 .get(&collection_id)
1612 .ok_or_else(|| StorageError::IdentifierMissing(collection_id))?
1613 .collection_metadata
1614 .clone();
1615 let instance = self.instances.get_mut(&instance_id).ok_or_else(|| {
1616 StorageError::Generic(anyhow::anyhow!("missing cluster {instance_id}"))
1618 })?;
1619 let oneshot_cmd = RunOneshotIngestion {
1620 ingestion_id,
1621 collection_id,
1622 collection_meta,
1623 request,
1624 };
1625
1626 if !self.read_only {
1627 instance.send(StorageCommand::RunOneshotIngestion(Box::new(oneshot_cmd)));
1628 let pending = PendingOneshotIngestion {
1629 result_tx,
1630 cluster_id: instance_id,
1631 };
1632 let novel = self
1633 .pending_oneshot_ingestions
1634 .insert(ingestion_id, pending);
1635 assert_none!(novel);
1636 Ok(())
1637 } else {
1638 Err(StorageError::ReadOnly)
1639 }
1640 }
1641
1642 fn cancel_oneshot_ingestion(&mut self, ingestion_id: uuid::Uuid) -> Result<(), StorageError> {
1643 if self.read_only {
1644 return Err(StorageError::ReadOnly);
1645 }
1646
1647 let pending = self
1648 .pending_oneshot_ingestions
1649 .remove(&ingestion_id)
1650 .ok_or_else(|| {
1651 StorageError::Generic(anyhow::anyhow!("missing oneshot ingestion {ingestion_id}"))
1653 })?;
1654
1655 match self.instances.get_mut(&pending.cluster_id) {
1656 Some(instance) => {
1657 instance.send(StorageCommand::CancelOneshotIngestion(ingestion_id));
1658 }
1659 None => {
1660 mz_ore::soft_panic_or_log!(
1661 "canceling oneshot ingestion on non-existent cluster, ingestion {:?}, instance {}",
1662 ingestion_id,
1663 pending.cluster_id,
1664 );
1665 }
1666 }
1667 pending.cancel();
1669
1670 Ok(())
1671 }
1672
1673 async fn alter_export(
1674 &mut self,
1675 id: GlobalId,
1676 new_description: ExportDescription,
1677 ) -> Result<(), StorageError> {
1678 let from_id = new_description.sink.from;
1679
1680 let desired_read_holds = vec![from_id.clone(), id.clone()];
1683 let [input_hold, self_hold] = self
1684 .storage_collections
1685 .acquire_read_holds(desired_read_holds)
1686 .expect("missing dependency")
1687 .try_into()
1688 .expect("expected number of holds");
1689 let from_storage_metadata = self.storage_collections.collection_metadata(from_id)?;
1690 let to_storage_metadata = self.storage_collections.collection_metadata(id)?;
1691
1692 let cur_export = self.export_mut(id)?;
1694 let input_readable = cur_export
1695 .write_frontier
1696 .iter()
1697 .all(|t| input_hold.since().less_than(t));
1698 if !input_readable {
1699 return Err(StorageError::ReadBeforeSince(from_id));
1700 }
1701
1702 let new_export = ExportState {
1703 read_capabilities: cur_export.read_capabilities.clone(),
1704 cluster_id: new_description.instance_id,
1705 derived_since: cur_export.derived_since.clone(),
1706 read_holds: [input_hold, self_hold],
1707 read_policy: cur_export.read_policy.clone(),
1708 write_frontier: cur_export.write_frontier.clone(),
1709 };
1710 *cur_export = new_export;
1711
1712 let with_snapshot = new_description.sink.with_snapshot
1719 && !PartialOrder::less_than(&new_description.sink.as_of, &cur_export.write_frontier);
1720
1721 let cmd = RunSinkCommand {
1722 id,
1723 description: StorageSinkDesc {
1724 from: from_id,
1725 from_desc: new_description.sink.from_desc.clone(),
1726 connection: new_description.sink.connection.clone(),
1727 envelope: new_description.sink.envelope,
1728 as_of: new_description.sink.as_of.clone(),
1729 version: new_description.sink.version,
1730 from_storage_metadata,
1731 with_snapshot,
1732 to_storage_metadata,
1733 commit_interval: new_description.sink.commit_interval,
1734 },
1735 };
1736
1737 let instance = self
1739 .instances
1740 .get_mut(&new_description.instance_id)
1741 .ok_or_else(|| StorageError::ExportInstanceMissing {
1742 storage_instance_id: new_description.instance_id,
1743 export_id: id,
1744 })?;
1745
1746 instance.send(StorageCommand::RunSink(Box::new(cmd)));
1747
1748 let state = self
1752 .collections
1753 .get_mut(&id)
1754 .expect("export known to exist");
1755 let DataSource::Sink { desc } = &mut state.data_source else {
1756 panic!("export known to exist")
1757 };
1758 *desc = new_description;
1759
1760 Ok(())
1761 }
1762
1763 async fn alter_export_connections(
1765 &mut self,
1766 exports: BTreeMap<GlobalId, StorageSinkConnection>,
1767 ) -> Result<(), StorageError> {
1768 let mut updates_by_instance =
1769 BTreeMap::<StorageInstanceId, Vec<(RunSinkCommand, ExportDescription)>>::new();
1770
1771 for (id, connection) in exports {
1772 let (current_export_description, as_of): (ExportDescription, _) = {
1780 let export = &self.collections[&id];
1781 let DataSource::Sink { desc } = &export.data_source else {
1782 panic!("export exists")
1783 };
1784 let CollectionStateExtra::Export(state) = &export.extra_state else {
1785 panic!("export exists")
1786 };
1787 let export_description = desc.clone();
1788 let as_of = state.input_hold().since().clone();
1789
1790 (export_description, as_of)
1791 };
1792
1793 let mut new_export_description = current_export_description.clone();
1794
1795 new_export_description.sink.connection = connection;
1796
1797 current_export_description
1799 .sink
1800 .alter_compatible(id, &new_export_description.sink)?;
1801
1802 let from_storage_metadata = self
1803 .storage_collections
1804 .collection_metadata(new_export_description.sink.from)?;
1805 let to_storage_metadata = self.storage_collections.collection_metadata(id)?;
1806
1807 let cmd = RunSinkCommand {
1808 id,
1809 description: StorageSinkDesc {
1810 from: new_export_description.sink.from,
1811 from_desc: new_export_description.sink.from_desc.clone(),
1812 connection: new_export_description.sink.connection.clone(),
1813 envelope: new_export_description.sink.envelope,
1814 with_snapshot: new_export_description.sink.with_snapshot,
1815 version: new_export_description.sink.version,
1816 as_of: as_of.to_owned(),
1827 from_storage_metadata,
1828 to_storage_metadata,
1829 commit_interval: new_export_description.sink.commit_interval,
1830 },
1831 };
1832
1833 let update = updates_by_instance
1834 .entry(new_export_description.instance_id)
1835 .or_default();
1836 update.push((cmd, new_export_description));
1837 }
1838
1839 for (instance_id, updates) in updates_by_instance {
1840 let mut export_updates = BTreeMap::new();
1841 let mut cmds = Vec::with_capacity(updates.len());
1842
1843 for (cmd, export_state) in updates {
1844 export_updates.insert(cmd.id, export_state);
1845 cmds.push(cmd);
1846 }
1847
1848 let instance = self.instances.get_mut(&instance_id).ok_or_else(|| {
1850 StorageError::ExportInstanceMissing {
1851 storage_instance_id: instance_id,
1852 export_id: *export_updates
1853 .keys()
1854 .next()
1855 .expect("set of exports not empty"),
1856 }
1857 })?;
1858
1859 for cmd in cmds {
1860 instance.send(StorageCommand::RunSink(Box::new(cmd)));
1861 }
1862
1863 for (id, new_export_description) in export_updates {
1865 let Some(state) = self.collections.get_mut(&id) else {
1866 panic!("export known to exist")
1867 };
1868 let DataSource::Sink { desc } = &mut state.data_source else {
1869 panic!("export known to exist")
1870 };
1871 *desc = new_export_description;
1872 }
1873 }
1874
1875 Ok(())
1876 }
1877
1878 fn drop_tables(
1879 &mut self,
1880 storage_metadata: &StorageMetadata,
1881 identifiers: Vec<GlobalId>,
1882 ) -> Result<(), StorageError> {
1883 let (table_write_ids, data_source_ids): (Vec<_>, Vec<_>) = identifiers
1884 .into_iter()
1885 .partition(|id| match self.collections[id].data_source {
1886 DataSource::Table => true,
1887 DataSource::IngestionExport { .. } | DataSource::Webhook => false,
1888 _ => panic!("identifier is not a table: {}", id),
1889 });
1890
1891 if table_write_ids.len() > 0 {
1892 let tx = self.pending_table_handle_drops_tx.clone();
1893 for identifier in table_write_ids {
1894 let _ = tx.send(identifier);
1895 }
1896 }
1897
1898 if data_source_ids.len() > 0 {
1899 self.validate_collection_ids(data_source_ids.iter().cloned())?;
1900 self.drop_sources_unvalidated(storage_metadata, data_source_ids)?;
1901 }
1902
1903 Ok(())
1904 }
1905
1906 fn drop_sources(
1907 &mut self,
1908 storage_metadata: &StorageMetadata,
1909 identifiers: Vec<GlobalId>,
1910 ) -> Result<(), StorageError> {
1911 self.validate_collection_ids(identifiers.iter().cloned())?;
1912 self.drop_sources_unvalidated(storage_metadata, identifiers)
1913 }
1914
1915 fn drop_sources_unvalidated(
1916 &mut self,
1917 storage_metadata: &StorageMetadata,
1918 ids: Vec<GlobalId>,
1919 ) -> Result<(), StorageError> {
1920 let mut ingestions_to_execute = BTreeSet::new();
1923 let mut ingestions_to_drop = BTreeSet::new();
1924 let mut source_statistics_to_drop = Vec::new();
1925
1926 let mut collections_to_drop = Vec::new();
1930
1931 for id in ids.iter() {
1932 let collection_state = self.collections.get(id);
1933
1934 if let Some(collection_state) = collection_state {
1935 match collection_state.data_source {
1936 DataSource::Webhook => {
1937 let fut = self.collection_manager.unregister_collection(*id);
1940 mz_ore::task::spawn(|| format!("storage-webhook-cleanup-{id}"), fut);
1941
1942 collections_to_drop.push(*id);
1943 source_statistics_to_drop.push(*id);
1944 }
1945 DataSource::Ingestion(_) => {
1946 ingestions_to_drop.insert(*id);
1947 source_statistics_to_drop.push(*id);
1948 }
1949 DataSource::IngestionExport { ingestion_id, .. } => {
1950 ingestions_to_execute.insert(ingestion_id);
1957
1958 let ingestion_state = match self.collections.get_mut(&ingestion_id) {
1960 Some(ingestion_collection) => ingestion_collection,
1961 None => {
1963 tracing::error!(
1964 "primary source {ingestion_id} seemingly dropped before subsource {id}"
1965 );
1966 continue;
1967 }
1968 };
1969
1970 match &mut ingestion_state.data_source {
1971 DataSource::Ingestion(ingestion_desc) => {
1972 let removed = ingestion_desc.source_exports.remove(id);
1973 mz_ore::soft_assert_or_log!(
1974 removed.is_some(),
1975 "dropped subsource {id} already removed from source exports"
1976 );
1977 }
1978 _ => unreachable!(
1979 "SourceExport must only refer to primary sources that already exist"
1980 ),
1981 };
1982
1983 ingestions_to_drop.insert(*id);
1987 source_statistics_to_drop.push(*id);
1988 }
1989 DataSource::Progress | DataSource::Table | DataSource::Other => {
1990 collections_to_drop.push(*id);
1991 }
1992 DataSource::Introspection(_) | DataSource::Sink { .. } => {
1993 soft_panic_or_log!(
1996 "drop_sources called on a {:?} (id={id}))",
1997 collection_state.data_source,
1998 );
1999 }
2000 }
2001 }
2002 }
2003
2004 ingestions_to_execute.retain(|id| !ingestions_to_drop.contains(id));
2006 for ingestion_id in ingestions_to_execute {
2007 self.run_ingestion(ingestion_id)?;
2008 }
2009
2010 let ingestion_policies = ingestions_to_drop
2017 .iter()
2018 .map(|id| (*id, ReadPolicy::ValidFrom(Antichain::new())))
2019 .collect();
2020
2021 tracing::debug!(
2022 ?ingestion_policies,
2023 "dropping sources by setting read hold policies"
2024 );
2025 self.set_hold_policies(ingestion_policies);
2026
2027 let shards_to_update: BTreeSet<_> = ingestions_to_drop
2029 .iter()
2030 .chain(collections_to_drop.iter())
2031 .cloned()
2032 .collect();
2033 self.append_shard_mappings(shards_to_update.into_iter(), Diff::MINUS_ONE);
2034
2035 let status_now = mz_ore::now::to_datetime((self.now)());
2036 let mut status_updates = vec![];
2037 for id in ingestions_to_drop.iter() {
2038 status_updates.push(StatusUpdate::new(*id, status_now, Status::Dropped));
2039 }
2040
2041 if !self.read_only {
2042 self.append_status_introspection_updates(
2043 IntrospectionType::SourceStatusHistory,
2044 status_updates,
2045 );
2046 }
2047
2048 {
2049 let mut source_statistics = self.source_statistics.lock().expect("poisoned");
2050 for id in source_statistics_to_drop {
2051 source_statistics
2052 .source_statistics
2053 .retain(|(stats_id, _), _| stats_id != &id);
2054 source_statistics
2055 .webhook_statistics
2056 .retain(|stats_id, _| stats_id != &id);
2057 }
2058 }
2059
2060 for id in ingestions_to_drop.iter().chain(collections_to_drop.iter()) {
2062 tracing::info!(%id, "dropping collection state");
2063 let collection = self
2064 .collections
2065 .remove(id)
2066 .expect("list populated after checking that self.collections contains it");
2067
2068 let instance = match &collection.extra_state {
2069 CollectionStateExtra::Ingestion(ingestion) => Some(ingestion.instance_id),
2070 CollectionStateExtra::Export(export) => Some(export.cluster_id()),
2071 CollectionStateExtra::None => None,
2072 }
2073 .and_then(|i| self.instances.get(&i));
2074
2075 if let Some(instance) = instance {
2079 let active_replicas = instance.get_active_replicas_for_object(id);
2080 if !active_replicas.is_empty() {
2081 match &collection.data_source {
2088 DataSource::Ingestion(ingestion_desc) => {
2089 if *id != ingestion_desc.remap_collection_id {
2090 self.dropped_objects.insert(
2091 ingestion_desc.remap_collection_id,
2092 active_replicas.clone(),
2093 );
2094 }
2095 }
2096 _ => {}
2097 }
2098
2099 self.dropped_objects.insert(*id, active_replicas);
2100 }
2101 }
2102 }
2103
2104 self.storage_collections
2106 .drop_collections_unvalidated(storage_metadata, ids);
2107
2108 Ok(())
2109 }
2110
2111 fn drop_sinks(
2113 &mut self,
2114 storage_metadata: &StorageMetadata,
2115 identifiers: Vec<GlobalId>,
2116 ) -> Result<(), StorageError> {
2117 self.validate_export_ids(identifiers.iter().cloned())?;
2118 self.drop_sinks_unvalidated(storage_metadata, identifiers);
2119 Ok(())
2120 }
2121
2122 fn drop_sinks_unvalidated(
2123 &mut self,
2124 storage_metadata: &StorageMetadata,
2125 mut sinks_to_drop: Vec<GlobalId>,
2126 ) {
2127 sinks_to_drop.retain(|id| self.export(*id).is_ok());
2129
2130 let drop_policy = sinks_to_drop
2137 .iter()
2138 .map(|id| (*id, ReadPolicy::ValidFrom(Antichain::new())))
2139 .collect();
2140
2141 tracing::debug!(
2142 ?drop_policy,
2143 "dropping sources by setting read hold policies"
2144 );
2145 self.set_hold_policies(drop_policy);
2146
2147 let status_now = mz_ore::now::to_datetime((self.now)());
2154
2155 let mut status_updates = vec![];
2157 {
2158 let mut sink_statistics = self.sink_statistics.lock().expect("poisoned");
2159 for id in sinks_to_drop.iter() {
2160 status_updates.push(StatusUpdate::new(*id, status_now, Status::Dropped));
2161 sink_statistics.retain(|(stats_id, _), _| stats_id != id);
2162 }
2163 }
2164
2165 if !self.read_only {
2166 self.append_status_introspection_updates(
2167 IntrospectionType::SinkStatusHistory,
2168 status_updates,
2169 );
2170 }
2171
2172 for id in sinks_to_drop.iter() {
2174 tracing::info!(%id, "dropping export state");
2175 let collection = self
2176 .collections
2177 .remove(id)
2178 .expect("list populated after checking that self.collections contains it");
2179
2180 let instance = match &collection.extra_state {
2181 CollectionStateExtra::Ingestion(ingestion) => Some(ingestion.instance_id),
2182 CollectionStateExtra::Export(export) => Some(export.cluster_id()),
2183 CollectionStateExtra::None => None,
2184 }
2185 .and_then(|i| self.instances.get(&i));
2186
2187 if let Some(instance) = instance {
2191 let active_replicas = instance.get_active_replicas_for_object(id);
2192 if !active_replicas.is_empty() {
2193 self.dropped_objects.insert(*id, active_replicas);
2194 }
2195 }
2196 }
2197
2198 self.storage_collections
2200 .drop_collections_unvalidated(storage_metadata, sinks_to_drop);
2201 }
2202
2203 #[instrument(level = "debug")]
2204 fn append_table(
2205 &mut self,
2206 write_ts: Timestamp,
2207 advance_to: Timestamp,
2208 commands: Vec<(GlobalId, Vec<TableData>)>,
2209 ) -> Result<tokio::sync::oneshot::Receiver<Result<(), StorageError>>, StorageError> {
2210 if self.read_only {
2211 if !commands
2214 .iter()
2215 .all(|(id, _)| id.is_system() && self.migrated_storage_collections.contains(id))
2216 {
2217 return Err(StorageError::ReadOnly);
2218 }
2219 }
2220
2221 for (id, updates) in commands.iter() {
2223 if !updates.is_empty() {
2224 if !write_ts.less_than(&advance_to) {
2225 return Err(StorageError::UpdateBeyondUpper(*id));
2226 }
2227 }
2228 }
2229
2230 Ok(self
2231 .persist_table_worker
2232 .append(write_ts, advance_to, commands))
2233 }
2234
2235 fn table_write_handle(&self) -> Arc<dyn mz_storage_client::controller::TableWriteHandle> {
2236 Arc::new(persist_handles::TableWriteWorkerHandle(
2237 self.persist_table_worker.clone(),
2238 ))
2239 }
2240
2241 fn monotonic_appender(&self, id: GlobalId) -> Result<MonotonicAppender, StorageError> {
2242 self.collection_manager.monotonic_appender(id)
2243 }
2244
2245 fn webhook_statistics(&self, id: GlobalId) -> Result<Arc<WebhookStatistics>, StorageError> {
2246 let source_statistics = self.source_statistics.lock().expect("poisoned");
2248 source_statistics
2249 .webhook_statistics
2250 .get(&id)
2251 .cloned()
2252 .ok_or(StorageError::IdentifierMissing(id))
2253 }
2254
2255 async fn ready(&mut self) {
2256 if self.maintenance_scheduled {
2257 return;
2258 }
2259
2260 if !self.pending_table_handle_drops_rx.is_empty() {
2261 return;
2262 }
2263
2264 tokio::select! {
2265 Some(m) = self.instance_response_rx.recv() => {
2266 self.stashed_responses.push(m);
2267 while let Ok(m) = self.instance_response_rx.try_recv() {
2268 self.stashed_responses.push(m);
2269 }
2270 }
2271 _ = self.maintenance_ticker.tick() => {
2272 self.maintenance_scheduled = true;
2273 },
2274 };
2275 }
2276
2277 #[instrument(level = "debug")]
2278 fn process(
2279 &mut self,
2280 storage_metadata: &StorageMetadata,
2281 ) -> Result<Option<Response>, anyhow::Error> {
2282 if self.maintenance_scheduled {
2284 self.maintain();
2285 self.maintenance_scheduled = false;
2286 }
2287
2288 for instance in self.instances.values_mut() {
2289 instance.rehydrate_failed_replicas();
2290 }
2291
2292 let mut status_updates = vec![];
2293 let mut updated_frontiers = BTreeMap::new();
2294
2295 let stashed_responses = std::mem::take(&mut self.stashed_responses);
2297 for resp in stashed_responses {
2298 match resp {
2299 (_replica_id, StorageResponse::FrontierUpper(id, upper)) => {
2300 self.update_write_frontier(id, &upper);
2301 updated_frontiers.insert(id, upper);
2302 }
2303 (replica_id, StorageResponse::DroppedId(id)) => {
2304 let replica_id = replica_id.expect("DroppedId from unknown replica");
2305 if let Some(remaining_replicas) = self.dropped_objects.get_mut(&id) {
2306 remaining_replicas.remove(&replica_id);
2307 if remaining_replicas.is_empty() {
2308 self.dropped_objects.remove(&id);
2309 }
2310 } else {
2311 soft_panic_or_log!("unexpected DroppedId for {id}");
2312 }
2313 }
2314 (replica_id, StorageResponse::StatisticsUpdates(source_stats, sink_stats)) => {
2315 {
2317 let replica_id = if let Some(replica_id) = replica_id {
2324 replica_id
2325 } else {
2326 tracing::error!(
2327 ?source_stats,
2328 "missing replica_id for source statistics update"
2329 );
2330 continue;
2331 };
2332
2333 let mut shared_stats = self.source_statistics.lock().expect("poisoned");
2334
2335 for stat in source_stats {
2336 let collection_id = stat.id.clone();
2337
2338 if self.collection(collection_id).is_err() {
2339 continue;
2342 }
2343
2344 let entry = shared_stats
2345 .source_statistics
2346 .entry((stat.id, Some(replica_id)));
2347
2348 match entry {
2349 btree_map::Entry::Vacant(vacant_entry) => {
2350 let mut stats = ControllerSourceStatistics::new(
2351 collection_id,
2352 Some(replica_id),
2353 );
2354 stats.incorporate(stat);
2355 vacant_entry.insert(stats);
2356 }
2357 btree_map::Entry::Occupied(mut occupied_entry) => {
2358 occupied_entry.get_mut().incorporate(stat);
2359 }
2360 }
2361 }
2362 }
2363
2364 {
2365 let replica_id = if let Some(replica_id) = replica_id {
2376 replica_id
2377 } else {
2378 tracing::error!(
2379 ?sink_stats,
2380 "missing replica_id for sink statistics update"
2381 );
2382 continue;
2383 };
2384
2385 let mut shared_stats = self.sink_statistics.lock().expect("poisoned");
2386
2387 for stat in sink_stats {
2388 let collection_id = stat.id.clone();
2389
2390 if self.collection(collection_id).is_err() {
2391 continue;
2394 }
2395
2396 let entry = shared_stats.entry((stat.id, Some(replica_id)));
2397
2398 match entry {
2399 btree_map::Entry::Vacant(vacant_entry) => {
2400 let mut stats =
2401 ControllerSinkStatistics::new(collection_id, replica_id);
2402 stats.incorporate(stat);
2403 vacant_entry.insert(stats);
2404 }
2405 btree_map::Entry::Occupied(mut occupied_entry) => {
2406 occupied_entry.get_mut().incorporate(stat);
2407 }
2408 }
2409 }
2410 }
2411 }
2412 (replica_id, StorageResponse::StatusUpdate(mut status_update)) => {
2413 match status_update.status {
2429 Status::Running => {
2430 let collection = self.collections.get_mut(&status_update.id);
2431 match collection {
2432 Some(collection) => {
2433 match collection.extra_state {
2434 CollectionStateExtra::Ingestion(
2435 ref mut ingestion_state,
2436 ) => {
2437 if ingestion_state.hydrated_on.is_empty() {
2438 tracing::debug!(ingestion_id = %status_update.id, "ingestion is hydrated");
2439 }
2440 ingestion_state.hydrated_on.insert(replica_id.expect(
2441 "replica id should be present for status running",
2442 ));
2443 }
2444 CollectionStateExtra::Export(_) => {
2445 }
2447 CollectionStateExtra::None => {
2448 }
2450 }
2451 }
2452 None => (), }
2455 }
2456 Status::Paused => {
2457 let collection = self.collections.get_mut(&status_update.id);
2458 match collection {
2459 Some(collection) => {
2460 match collection.extra_state {
2461 CollectionStateExtra::Ingestion(
2462 ref mut ingestion_state,
2463 ) => {
2464 tracing::debug!(ingestion_id = %status_update.id, "ingestion is now paused");
2471 ingestion_state.hydrated_on.clear();
2472 }
2473 CollectionStateExtra::Export(_) => {
2474 }
2476 CollectionStateExtra::None => {
2477 }
2479 }
2480 }
2481 None => (), }
2484 }
2485 _ => (),
2486 }
2487
2488 if let Some(id) = replica_id {
2490 status_update.replica_id = Some(id);
2491 }
2492 status_updates.push(status_update);
2493 }
2494 (_replica_id, StorageResponse::StagedBatches(batches)) => {
2495 for (ingestion_id, batches) in batches {
2496 match self.pending_oneshot_ingestions.remove(&ingestion_id) {
2497 Some(pending) => {
2498 if let Some(instance) = self.instances.get_mut(&pending.cluster_id)
2501 {
2502 instance
2503 .send(StorageCommand::CancelOneshotIngestion(ingestion_id));
2504 }
2505 (pending.result_tx)(batches)
2507 }
2508 None => {
2509 }
2512 }
2513 }
2514 }
2515 }
2516 }
2517
2518 self.record_status_updates(status_updates);
2519
2520 let mut dropped_table_ids = Vec::new();
2522 while let Ok(dropped_id) = self.pending_table_handle_drops_rx.try_recv() {
2523 dropped_table_ids.push(dropped_id);
2524 }
2525 if !dropped_table_ids.is_empty() {
2526 self.drop_sources(storage_metadata, dropped_table_ids)?;
2527 }
2528
2529 if updated_frontiers.is_empty() {
2530 Ok(None)
2531 } else {
2532 Ok(Some(Response::FrontierUpdates(
2533 updated_frontiers.into_iter().collect(),
2534 )))
2535 }
2536 }
2537
2538 async fn inspect_persist_state(
2539 &self,
2540 id: GlobalId,
2541 ) -> Result<serde_json::Value, anyhow::Error> {
2542 let collection = &self.storage_collections.collection_metadata(id)?;
2543 let client = self
2544 .persist
2545 .open(collection.persist_location.clone())
2546 .await?;
2547 let shard_state = client
2548 .inspect_shard::<Timestamp>(&collection.data_shard)
2549 .await?;
2550 let json_state = serde_json::to_value(shard_state)?;
2551 Ok(json_state)
2552 }
2553
2554 fn append_introspection_updates(
2555 &mut self,
2556 type_: IntrospectionType,
2557 updates: Vec<(Row, Diff)>,
2558 ) {
2559 let id = self.introspection_ids[&type_];
2560 let updates = updates.into_iter().map(|update| update.into()).collect();
2561 self.collection_manager.blind_write(id, updates);
2562 }
2563
2564 fn append_status_introspection_updates(
2565 &mut self,
2566 type_: IntrospectionType,
2567 updates: Vec<StatusUpdate>,
2568 ) {
2569 let id = self.introspection_ids[&type_];
2570 let updates: Vec<_> = updates.into_iter().map(|update| update.into()).collect();
2571 if !updates.is_empty() {
2572 self.collection_manager.blind_write(id, updates);
2573 }
2574 }
2575
2576 fn update_introspection_collection(&mut self, type_: IntrospectionType, op: StorageWriteOp) {
2577 let id = self.introspection_ids[&type_];
2578 self.collection_manager.differential_write(id, op);
2579 }
2580
2581 fn append_only_introspection_tx(
2582 &self,
2583 type_: IntrospectionType,
2584 ) -> mpsc::UnboundedSender<(
2585 Vec<AppendOnlyUpdate>,
2586 oneshot::Sender<Result<(), StorageError>>,
2587 )> {
2588 let id = self.introspection_ids[&type_];
2589 self.collection_manager.append_only_write_sender(id)
2590 }
2591
2592 fn differential_introspection_tx(
2593 &self,
2594 type_: IntrospectionType,
2595 ) -> mpsc::UnboundedSender<(StorageWriteOp, oneshot::Sender<Result<(), StorageError>>)> {
2596 let id = self.introspection_ids[&type_];
2597 self.collection_manager.differential_write_sender(id)
2598 }
2599
2600 async fn real_time_recent_timestamp(
2601 &self,
2602 timestamp_objects: BTreeSet<GlobalId>,
2603 timeout: Duration,
2604 ) -> Result<BoxFuture<Result<Timestamp, StorageError>>, StorageError> {
2605 use mz_storage_types::sources::GenericSourceConnection;
2606
2607 let mut rtr_futures = BTreeMap::new();
2608
2609 for id in timestamp_objects.into_iter().filter(GlobalId::is_user) {
2611 let collection = match self.collection(id) {
2612 Ok(c) => c,
2613 Err(_) => continue,
2615 };
2616
2617 let (source_conn, remap_id) = match &collection.data_source {
2618 DataSource::Ingestion(IngestionDescription {
2619 desc: SourceDesc { connection, .. },
2620 remap_collection_id,
2621 ..
2622 }) => match connection {
2623 GenericSourceConnection::Kafka(_)
2624 | GenericSourceConnection::Postgres(_)
2625 | GenericSourceConnection::MySql(_)
2626 | GenericSourceConnection::SqlServer(_) => {
2627 (connection.clone(), *remap_collection_id)
2628 }
2629
2630 GenericSourceConnection::LoadGenerator(_) => continue,
2635 },
2636 _ => {
2638 continue;
2639 }
2640 };
2641
2642 let config = self.config().clone();
2644
2645 let read_handle = self.read_handle_for_snapshot(remap_id).await?;
2653
2654 let remap_read_hold = self
2657 .storage_collections
2658 .acquire_read_holds(vec![remap_id])
2659 .map_err(|_e| StorageError::ReadBeforeSince(remap_id))?
2660 .expect_element(|| "known to be exactly one");
2661
2662 let remap_as_of = remap_read_hold
2663 .since()
2664 .to_owned()
2665 .into_option()
2666 .ok_or(StorageError::ReadBeforeSince(remap_id))?;
2667
2668 rtr_futures.insert(
2669 id,
2670 tokio::time::timeout(timeout, async move {
2671 use mz_storage_types::sources::SourceConnection as _;
2672
2673 let as_of = Antichain::from_elem(remap_as_of);
2676 let remap_subscribe = read_handle
2677 .subscribe(as_of.clone())
2678 .await
2679 .map_err(|_| StorageError::ReadBeforeSince(remap_id))?;
2680
2681 tracing::debug!(?id, type_ = source_conn.name(), upstream = ?source_conn.external_reference(), "fetching real time recency");
2682
2683 let result = rtr::real_time_recency_ts(
2684 source_conn,
2685 id,
2686 config,
2687 as_of,
2688 remap_subscribe,
2689 )
2690 .await
2691 .map_err(|e| {
2692 tracing::debug!(?id, "real time recency error: {:?}", e);
2693 e
2694 });
2695
2696 drop(remap_read_hold);
2698
2699 result
2700 }),
2701 );
2702 }
2703
2704 Ok(Box::pin(async move {
2705 let (ids, futs): (Vec<_>, Vec<_>) = rtr_futures.into_iter().unzip();
2706 ids.into_iter()
2707 .zip_eq(futures::future::join_all(futs).await)
2708 .try_fold(Timestamp::MIN, |curr, (id, per_source_res)| {
2709 let new =
2710 per_source_res.map_err(|_e: Elapsed| StorageError::RtrTimeout(id))??;
2711 Ok::<_, StorageError>(std::cmp::max(curr, new))
2712 })
2713 }))
2714 }
2715
2716 fn dump(&self) -> Result<serde_json::Value, anyhow::Error> {
2717 let Self {
2719 build_info: _,
2720 now: _,
2721 read_only,
2722 collections,
2723 dropped_objects,
2724 persist_table_worker: _,
2725 txns_read: _,
2726 txns_metrics: _,
2727 stashed_responses,
2728 pending_table_handle_drops_tx: _,
2729 pending_table_handle_drops_rx: _,
2730 pending_oneshot_ingestions,
2731 collection_manager: _,
2732 introspection_ids,
2733 introspection_tokens: _,
2734 source_statistics: _,
2735 sink_statistics: _,
2736 statistics_interval_sender: _,
2737 instances,
2738 replica_dyncfg_overrides: _,
2739 initialized,
2740 config,
2741 persist_location,
2742 persist: _,
2743 metrics: _,
2744 recorded_frontiers,
2745 recorded_replica_frontiers,
2746 wallclock_lag: _,
2747 wallclock_lag_last_recorded,
2748 storage_collections: _,
2749 migrated_storage_collections,
2750 maintenance_ticker: _,
2751 maintenance_scheduled,
2752 instance_response_tx: _,
2753 instance_response_rx: _,
2754 persist_warm_task: _,
2755 } = self;
2756
2757 let collections: BTreeMap<_, _> = collections
2758 .iter()
2759 .map(|(id, c)| (id.to_string(), format!("{c:?}")))
2760 .collect();
2761 let dropped_objects: BTreeMap<_, _> = dropped_objects
2762 .iter()
2763 .map(|(id, rs)| (id.to_string(), format!("{rs:?}")))
2764 .collect();
2765 let stashed_responses: Vec<_> =
2766 stashed_responses.iter().map(|r| format!("{r:?}")).collect();
2767 let pending_oneshot_ingestions: BTreeMap<_, _> = pending_oneshot_ingestions
2768 .iter()
2769 .map(|(uuid, i)| (uuid.to_string(), format!("{i:?}")))
2770 .collect();
2771 let introspection_ids: BTreeMap<_, _> = introspection_ids
2772 .iter()
2773 .map(|(typ, id)| (format!("{typ:?}"), id.to_string()))
2774 .collect();
2775 let instances: BTreeMap<_, _> = instances
2776 .iter()
2777 .map(|(id, i)| (id.to_string(), format!("{i:?}")))
2778 .collect();
2779 let recorded_frontiers: BTreeMap<_, _> = recorded_frontiers
2780 .iter()
2781 .map(|(id, fs)| (id.to_string(), format!("{fs:?}")))
2782 .collect();
2783 let recorded_replica_frontiers: Vec<_> = recorded_replica_frontiers
2784 .iter()
2785 .map(|((gid, rid), f)| (gid.to_string(), rid.to_string(), format!("{f:?}")))
2786 .collect();
2787 let migrated_storage_collections: Vec<_> = migrated_storage_collections
2788 .iter()
2789 .map(|id| id.to_string())
2790 .collect();
2791
2792 Ok(serde_json::json!({
2793 "read_only": read_only,
2794 "collections": collections,
2795 "dropped_objects": dropped_objects,
2796 "stashed_responses": stashed_responses,
2797 "pending_oneshot_ingestions": pending_oneshot_ingestions,
2798 "introspection_ids": introspection_ids,
2799 "instances": instances,
2800 "initialized": initialized,
2801 "config": format!("{config:?}"),
2802 "persist_location": format!("{persist_location:?}"),
2803 "recorded_frontiers": recorded_frontiers,
2804 "recorded_replica_frontiers": recorded_replica_frontiers,
2805 "wallclock_lag_last_recorded": format!("{wallclock_lag_last_recorded:?}"),
2806 "migrated_storage_collections": migrated_storage_collections,
2807 "maintenance_scheduled": maintenance_scheduled,
2808 }))
2809 }
2810}
2811
2812pub fn prepare_initialization(txn: &mut dyn StorageTxn) -> Result<(), StorageError> {
2819 if txn.get_txn_wal_shard().is_none() {
2820 let txns_id = ShardId::new();
2821 txn.write_txn_wal_shard(txns_id)?;
2822 }
2823
2824 Ok(())
2825}
2826
2827impl Controller
2828where
2829 Self: StorageController,
2830{
2831 pub async fn new(
2839 build_info: &'static BuildInfo,
2840 persist_location: PersistLocation,
2841 persist_clients: Arc<PersistClientCache>,
2842 now: NowFn,
2843 wallclock_lag: WallclockLagFn<Timestamp>,
2844 txns_metrics: Arc<TxnMetrics>,
2845 read_only: bool,
2846 metrics_registry: &MetricsRegistry,
2847 controller_metrics: ControllerMetrics,
2848 connection_context: ConnectionContext,
2849 txn: &dyn StorageTxn,
2850 storage_collections: Arc<dyn StorageCollections + Send + Sync>,
2851 ) -> Self {
2852 let txns_client = persist_clients
2853 .open(persist_location.clone())
2854 .await
2855 .expect("location should be valid");
2856
2857 let persist_warm_task = warm_persist_state_in_background(
2858 txns_client.clone(),
2859 txn.get_collection_metadata().into_values(),
2860 );
2861 let persist_warm_task = Some(persist_warm_task.abort_on_drop());
2862
2863 let txns_id = txn
2867 .get_txn_wal_shard()
2868 .expect("must call prepare initialization before creating storage controller");
2869
2870 let persist_table_worker = if read_only {
2871 let txns_write = txns_client
2872 .open_writer(
2873 txns_id,
2874 Arc::new(TxnsCodecRow::desc()),
2875 Arc::new(UnitSchema),
2876 Diagnostics {
2877 shard_name: "txns".to_owned(),
2878 handle_purpose: "follow txns upper".to_owned(),
2879 },
2880 )
2881 .await
2882 .expect("txns schema shouldn't change");
2883 persist_handles::PersistTableWriteWorker::new_read_only_mode(
2884 txns_write,
2885 txns_client.clone(),
2886 )
2887 } else {
2888 let mut txns = TxnsHandle::open(
2889 Timestamp::MIN,
2890 txns_client.clone(),
2891 txns_client.dyncfgs().clone(),
2892 Arc::clone(&txns_metrics),
2893 txns_id,
2894 Opaque::encode(&PersistEpoch::default()),
2895 )
2896 .await;
2897 txns.upgrade_version().await;
2898 persist_handles::PersistTableWriteWorker::new_txns(txns, txns_client.clone())
2899 };
2900 let txns_read = TxnsRead::start::<TxnsCodecRow>(txns_client.clone(), txns_id).await;
2901
2902 let collection_manager = collection_mgmt::CollectionManager::new(read_only, now.clone());
2903
2904 let introspection_ids = BTreeMap::new();
2905 let introspection_tokens = Arc::new(Mutex::new(BTreeMap::new()));
2906
2907 let (statistics_interval_sender, _) =
2908 channel(mz_storage_types::parameters::STATISTICS_INTERVAL_DEFAULT);
2909
2910 let (pending_table_handle_drops_tx, pending_table_handle_drops_rx) =
2911 tokio::sync::mpsc::unbounded_channel();
2912
2913 let mut maintenance_ticker = tokio::time::interval(Duration::from_secs(1));
2914 maintenance_ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
2915
2916 let (instance_response_tx, instance_response_rx) = mpsc::unbounded_channel();
2917
2918 let metrics = StorageControllerMetrics::new(metrics_registry, controller_metrics);
2919
2920 let now_dt = mz_ore::now::to_datetime(now());
2921
2922 Self {
2923 build_info,
2924 collections: BTreeMap::default(),
2925 dropped_objects: Default::default(),
2926 persist_table_worker,
2927 txns_read,
2928 txns_metrics,
2929 stashed_responses: vec![],
2930 pending_table_handle_drops_tx,
2931 pending_table_handle_drops_rx,
2932 pending_oneshot_ingestions: BTreeMap::default(),
2933 collection_manager,
2934 introspection_ids,
2935 introspection_tokens,
2936 now,
2937 read_only,
2938 source_statistics: Arc::new(Mutex::new(statistics::SourceStatistics {
2939 source_statistics: BTreeMap::new(),
2940 webhook_statistics: BTreeMap::new(),
2941 })),
2942 sink_statistics: Arc::new(Mutex::new(BTreeMap::new())),
2943 statistics_interval_sender,
2944 instances: BTreeMap::new(),
2945 replica_dyncfg_overrides: BTreeMap::new(),
2946 initialized: false,
2947 config: StorageConfiguration::new(connection_context, mz_dyncfgs::all_dyncfgs()),
2948 persist_location,
2949 persist: persist_clients,
2950 metrics,
2951 recorded_frontiers: BTreeMap::new(),
2952 recorded_replica_frontiers: BTreeMap::new(),
2953 wallclock_lag,
2954 wallclock_lag_last_recorded: now_dt,
2955 storage_collections,
2956 migrated_storage_collections: BTreeSet::new(),
2957 maintenance_ticker,
2958 maintenance_scheduled: false,
2959 instance_response_rx,
2960 instance_response_tx,
2961 persist_warm_task,
2962 }
2963 }
2964
2965 #[instrument(level = "debug")]
2973 fn set_hold_policies(&mut self, policies: Vec<(GlobalId, ReadPolicy)>) {
2974 let mut read_capability_changes = BTreeMap::default();
2975
2976 for (id, policy) in policies.into_iter() {
2977 if let Some(collection) = self.collections.get_mut(&id) {
2978 let (write_frontier, derived_since, hold_policy) = match &mut collection.extra_state
2979 {
2980 CollectionStateExtra::Ingestion(ingestion) => (
2981 ingestion.write_frontier.borrow(),
2982 &mut ingestion.derived_since,
2983 &mut ingestion.hold_policy,
2984 ),
2985 CollectionStateExtra::None => {
2986 unreachable!("set_hold_policies is only called for ingestions");
2987 }
2988 CollectionStateExtra::Export(export) => (
2989 export.write_frontier.borrow(),
2990 &mut export.derived_since,
2991 &mut export.read_policy,
2992 ),
2993 };
2994
2995 let new_derived_since = policy.frontier(write_frontier);
2996 let mut update = swap_updates(derived_since, new_derived_since);
2997 if !update.is_empty() {
2998 read_capability_changes.insert(id, update);
2999 }
3000
3001 *hold_policy = policy;
3002 }
3003 }
3004
3005 if !read_capability_changes.is_empty() {
3006 self.update_hold_capabilities(&mut read_capability_changes);
3007 }
3008 }
3009
3010 #[instrument(level = "debug", fields(updates))]
3011 fn update_write_frontier(&mut self, id: GlobalId, new_upper: &Antichain<Timestamp>) {
3012 let mut read_capability_changes = BTreeMap::default();
3013
3014 if let Some(collection) = self.collections.get_mut(&id) {
3015 let (write_frontier, derived_since, hold_policy) = match &mut collection.extra_state {
3016 CollectionStateExtra::Ingestion(ingestion) => (
3017 &mut ingestion.write_frontier,
3018 &mut ingestion.derived_since,
3019 &ingestion.hold_policy,
3020 ),
3021 CollectionStateExtra::None => {
3022 if matches!(collection.data_source, DataSource::Progress) {
3023 } else {
3025 tracing::error!(
3026 ?collection,
3027 ?new_upper,
3028 "updated write frontier for collection which is not an ingestion"
3029 );
3030 }
3031 return;
3032 }
3033 CollectionStateExtra::Export(export) => (
3034 &mut export.write_frontier,
3035 &mut export.derived_since,
3036 &export.read_policy,
3037 ),
3038 };
3039
3040 if PartialOrder::less_than(write_frontier, new_upper) {
3041 write_frontier.clone_from(new_upper);
3042 }
3043
3044 let new_derived_since = hold_policy.frontier(write_frontier.borrow());
3045 let mut update = swap_updates(derived_since, new_derived_since);
3046 if !update.is_empty() {
3047 read_capability_changes.insert(id, update);
3048 }
3049 } else if self.dropped_objects.contains_key(&id) {
3050 } else {
3053 soft_panic_or_log!("spurious upper update for {id}: {new_upper:?}");
3054 }
3055
3056 if !read_capability_changes.is_empty() {
3057 self.update_hold_capabilities(&mut read_capability_changes);
3058 }
3059 }
3060
3061 #[instrument(level = "debug", fields(updates))]
3065 fn update_hold_capabilities(
3066 &mut self,
3067 updates: &mut BTreeMap<GlobalId, ChangeBatch<Timestamp>>,
3068 ) {
3069 let mut collections_net = BTreeMap::new();
3071
3072 while let Some(key) = updates.keys().rev().next().cloned() {
3077 let mut update = updates.remove(&key).unwrap();
3078
3079 if key.is_user() {
3080 debug!(id = %key, ?update, "update_hold_capability");
3081 }
3082
3083 if let Some(collection) = self.collections.get_mut(&key) {
3084 match &mut collection.extra_state {
3085 CollectionStateExtra::Ingestion(ingestion) => {
3086 let changes = ingestion.read_capabilities.update_iter(update.drain());
3087 update.extend(changes);
3088
3089 let (changes, frontier, _cluster_id) =
3090 collections_net.entry(key).or_insert_with(|| {
3091 (
3092 <ChangeBatch<_>>::new(),
3093 Antichain::new(),
3094 ingestion.instance_id,
3095 )
3096 });
3097
3098 changes.extend(update.drain());
3099 *frontier = ingestion.read_capabilities.frontier().to_owned();
3100 }
3101 CollectionStateExtra::None => {
3102 soft_panic_or_log!(
3104 "trying to update holds for collection {collection:?} which is not \
3105 an ingestion: {update:?}"
3106 );
3107 continue;
3108 }
3109 CollectionStateExtra::Export(export) => {
3110 let changes = export.read_capabilities.update_iter(update.drain());
3111 update.extend(changes);
3112
3113 let (changes, frontier, _cluster_id) =
3114 collections_net.entry(key).or_insert_with(|| {
3115 (<ChangeBatch<_>>::new(), Antichain::new(), export.cluster_id)
3116 });
3117
3118 changes.extend(update.drain());
3119 *frontier = export.read_capabilities.frontier().to_owned();
3120 }
3121 }
3122 } else {
3123 tracing::warn!(id = ?key, ?update, "update_hold_capabilities for unknown object");
3125 }
3126 }
3127
3128 for (key, (mut changes, frontier, cluster_id)) in collections_net {
3131 if !changes.is_empty() {
3132 if key.is_user() {
3133 debug!(id = %key, ?frontier, "downgrading ingestion read holds!");
3134 }
3135
3136 let collection = self
3137 .collections
3138 .get_mut(&key)
3139 .expect("missing collection state");
3140
3141 let read_holds = match &mut collection.extra_state {
3142 CollectionStateExtra::Ingestion(ingestion) => {
3143 ingestion.dependency_read_holds.as_mut_slice()
3144 }
3145 CollectionStateExtra::Export(export) => export.read_holds.as_mut_slice(),
3146 CollectionStateExtra::None => {
3147 soft_panic_or_log!(
3148 "trying to downgrade read holds for collection which is not an \
3149 ingestion: {collection:?}"
3150 );
3151 continue;
3152 }
3153 };
3154
3155 for read_hold in read_holds.iter_mut() {
3156 read_hold
3157 .try_downgrade(frontier.clone())
3158 .expect("we only advance the frontier");
3159 }
3160
3161 if let Some(instance) = self.instances.get_mut(&cluster_id) {
3163 instance.send(StorageCommand::AllowCompaction(key, frontier.clone()));
3164 } else {
3165 soft_panic_or_log!(
3166 "missing instance client for cluster {cluster_id} while we still have outstanding AllowCompaction command {frontier:?} for {key}"
3167 );
3168 }
3169 }
3170 }
3171 }
3172
3173 fn validate_collection_ids(
3175 &self,
3176 ids: impl Iterator<Item = GlobalId>,
3177 ) -> Result<(), StorageError> {
3178 for id in ids {
3179 self.storage_collections.check_exists(id)?;
3180 }
3181 Ok(())
3182 }
3183
3184 fn validate_export_ids(&self, ids: impl Iterator<Item = GlobalId>) -> Result<(), StorageError> {
3186 for id in ids {
3187 self.export(id)?;
3188 }
3189 Ok(())
3190 }
3191
3192 async fn open_data_handles(
3194 &self,
3195 id: &GlobalId,
3196 shard: ShardId,
3197 relation_desc: RelationDesc,
3198 persist_client: &PersistClient,
3199 ) -> WriteHandle<SourceData, (), Timestamp, StorageDiff> {
3200 let diagnostics = Diagnostics {
3201 shard_name: id.to_string(),
3202 handle_purpose: format!("controller data for {}", id),
3203 };
3204
3205 let mut write = persist_client
3206 .open_writer(
3207 shard,
3208 Arc::new(relation_desc),
3209 Arc::new(UnitSchema),
3210 diagnostics.clone(),
3211 )
3212 .await
3213 .expect("invalid persist usage");
3214
3215 write.fetch_recent_upper().await;
3217
3218 write
3219 }
3220
3221 fn register_introspection_collection(
3226 &mut self,
3227 id: GlobalId,
3228 introspection_type: IntrospectionType,
3229 write_handle: WriteHandle<SourceData, (), Timestamp, StorageDiff>,
3230 persist_client: PersistClient,
3231 ) -> Result<(), StorageError> {
3232 tracing::info!(%id, ?introspection_type, "registering introspection collection");
3233
3234 let force_writable = self.read_only && self.migrated_storage_collections.contains(&id);
3242 if force_writable {
3243 assert!(id.is_system(), "unexpected non-system global id: {id:?}");
3244 info!("writing to migrated storage collection {id} in read-only mode");
3245 }
3246
3247 let prev = self.introspection_ids.insert(introspection_type, id);
3248 assert!(
3249 prev.is_none(),
3250 "cannot have multiple IDs for introspection type"
3251 );
3252
3253 let metadata = self.storage_collections.collection_metadata(id)?.clone();
3254
3255 let read_handle_fn = move || {
3256 let persist_client = persist_client.clone();
3257 let metadata = metadata.clone();
3258
3259 let fut = async move {
3260 let read_handle = persist_client
3261 .open_leased_reader::<SourceData, (), Timestamp, StorageDiff>(
3262 metadata.data_shard,
3263 Arc::new(metadata.relation_desc.clone()),
3264 Arc::new(UnitSchema),
3265 Diagnostics {
3266 shard_name: id.to_string(),
3267 handle_purpose: format!("snapshot {}", id),
3268 },
3269 USE_CRITICAL_SINCE_SNAPSHOT.get(persist_client.dyncfgs()),
3270 )
3271 .await
3272 .expect("invalid persist usage");
3273 read_handle
3274 };
3275
3276 fut.boxed()
3277 };
3278
3279 let recent_upper = write_handle.shared_upper();
3280
3281 match CollectionManagerKind::from(&introspection_type) {
3282 CollectionManagerKind::Differential => {
3287 let statistics_retention_duration =
3288 dyncfgs::STATISTICS_RETENTION_DURATION.get(self.config().config_set());
3289
3290 let introspection_config = DifferentialIntrospectionConfig {
3292 recent_upper,
3293 introspection_type,
3294 storage_collections: Arc::clone(&self.storage_collections),
3295 collection_manager: self.collection_manager.clone(),
3296 source_statistics: Arc::clone(&self.source_statistics),
3297 sink_statistics: Arc::clone(&self.sink_statistics),
3298 statistics_interval: self.config.parameters.statistics_interval.clone(),
3299 statistics_interval_receiver: self.statistics_interval_sender.subscribe(),
3300 statistics_retention_duration,
3301 metrics: self.metrics.clone(),
3302 introspection_tokens: Arc::clone(&self.introspection_tokens),
3303 };
3304 self.collection_manager.register_differential_collection(
3305 id,
3306 write_handle,
3307 read_handle_fn,
3308 force_writable,
3309 introspection_config,
3310 );
3311 }
3312 CollectionManagerKind::AppendOnly => {
3320 let introspection_config = AppendOnlyIntrospectionConfig {
3321 introspection_type,
3322 config_set: Arc::clone(self.config.config_set()),
3323 parameters: self.config.parameters.clone(),
3324 storage_collections: Arc::clone(&self.storage_collections),
3325 };
3326 self.collection_manager.register_append_only_collection(
3327 id,
3328 write_handle,
3329 force_writable,
3330 Some(introspection_config),
3331 );
3332 }
3333 }
3334
3335 Ok(())
3336 }
3337
3338 fn reconcile_dangling_statistics(&self) {
3341 self.source_statistics
3342 .lock()
3343 .expect("poisoned")
3344 .source_statistics
3345 .retain(|(k, _replica_id), _| self.storage_collections.check_exists(*k).is_ok());
3347 self.sink_statistics
3348 .lock()
3349 .expect("poisoned")
3350 .retain(|(k, _replica_id), _| self.export(*k).is_ok());
3351 }
3352
3353 #[instrument(level = "debug")]
3363 fn append_shard_mappings<I>(&self, global_ids: I, diff: Diff)
3364 where
3365 I: Iterator<Item = GlobalId>,
3366 {
3367 mz_ore::soft_assert_or_log!(
3368 diff == Diff::MINUS_ONE || diff == Diff::ONE,
3369 "use 1 for insert or -1 for delete"
3370 );
3371
3372 let id = *self
3373 .introspection_ids
3374 .get(&IntrospectionType::ShardMapping)
3375 .expect("should be registered before this call");
3376
3377 let mut updates = vec![];
3378 let mut row_buf = Row::default();
3380
3381 for global_id in global_ids {
3382 let shard_id = if let Some(collection) = self.collections.get(&global_id) {
3383 collection.collection_metadata.data_shard.clone()
3384 } else {
3385 panic!("unknown global id: {}", global_id);
3386 };
3387
3388 let mut packer = row_buf.packer();
3389 packer.push(Datum::from(global_id.to_string().as_str()));
3390 packer.push(Datum::from(shard_id.to_string().as_str()));
3391 updates.push((row_buf.clone(), diff));
3392 }
3393
3394 self.collection_manager.differential_append(id, updates);
3395 }
3396
3397 fn determine_collection_dependencies(
3399 &self,
3400 self_id: GlobalId,
3401 collection_desc: &CollectionDescription,
3402 ) -> Result<Vec<GlobalId>, StorageError> {
3403 let mut dependencies = Vec::new();
3404
3405 if let Some(id) = collection_desc.primary {
3406 dependencies.push(id);
3407 }
3408
3409 match &collection_desc.data_source {
3410 DataSource::Introspection(_)
3411 | DataSource::Webhook
3412 | DataSource::Table
3413 | DataSource::Progress
3414 | DataSource::Other => (),
3415 DataSource::IngestionExport { ingestion_id, .. } => {
3416 let source_collection = self.collection(*ingestion_id)?;
3419 let ingestion_remap_collection_id = match &source_collection.data_source {
3420 DataSource::Ingestion(ingestion) => ingestion.remap_collection_id,
3421 _ => unreachable!(
3422 "SourceExport must only refer to primary sources that already exist"
3423 ),
3424 };
3425
3426 dependencies.extend([self_id, ingestion_remap_collection_id]);
3432 }
3433 DataSource::Ingestion(ingestion) => {
3435 dependencies.push(self_id);
3440 if self_id != ingestion.remap_collection_id {
3441 dependencies.push(ingestion.remap_collection_id);
3442 }
3443 }
3444 DataSource::Sink { desc } => {
3445 dependencies.extend([self_id, desc.sink.from]);
3447 }
3448 };
3449
3450 Ok(dependencies)
3451 }
3452
3453 async fn read_handle_for_snapshot(
3454 &self,
3455 id: GlobalId,
3456 ) -> Result<ReadHandle<SourceData, (), Timestamp, StorageDiff>, StorageError> {
3457 let metadata = self.storage_collections.collection_metadata(id)?;
3458 read_handle_for_snapshot(&self.persist, id, &metadata).await
3459 }
3460
3461 fn record_status_updates(&mut self, updates: Vec<StatusUpdate>) {
3464 if self.read_only {
3465 return;
3466 }
3467
3468 let mut sink_status_updates = vec![];
3469 let mut source_status_updates = vec![];
3470
3471 for update in updates {
3472 let id = update.id;
3473 if self.export(id).is_ok() {
3474 sink_status_updates.push(update);
3475 } else if self.storage_collections.check_exists(id).is_ok() {
3476 source_status_updates.push(update);
3477 }
3478 }
3479
3480 self.append_status_introspection_updates(
3481 IntrospectionType::SourceStatusHistory,
3482 source_status_updates,
3483 );
3484 self.append_status_introspection_updates(
3485 IntrospectionType::SinkStatusHistory,
3486 sink_status_updates,
3487 );
3488 }
3489
3490 fn collection(&self, id: GlobalId) -> Result<&CollectionState, StorageError> {
3491 self.collections
3492 .get(&id)
3493 .ok_or(StorageError::IdentifierMissing(id))
3494 }
3495
3496 fn run_ingestion(&mut self, id: GlobalId) -> Result<(), StorageError> {
3499 tracing::info!(%id, "starting ingestion");
3500
3501 let collection = self.collection(id)?;
3502 let ingestion_description = match &collection.data_source {
3503 DataSource::Ingestion(i) => i.clone(),
3504 _ => {
3505 tracing::warn!("run_ingestion called on non-ingestion ID {}", id);
3506 Err(StorageError::IdentifierInvalid(id))?
3507 }
3508 };
3509
3510 let mut source_exports = BTreeMap::new();
3512 for (export_id, export) in ingestion_description.source_exports.clone() {
3513 let export_storage_metadata = self.collection(export_id)?.collection_metadata.clone();
3514 source_exports.insert(
3515 export_id,
3516 SourceExport {
3517 storage_metadata: export_storage_metadata,
3518 details: export.details,
3519 data_config: export.data_config,
3520 },
3521 );
3522 }
3523
3524 let remap_collection = self.collection(ingestion_description.remap_collection_id)?;
3525
3526 let description = IngestionDescription::<CollectionMetadata> {
3527 source_exports,
3528 remap_metadata: remap_collection.collection_metadata.clone(),
3529 desc: ingestion_description.desc.clone(),
3531 instance_id: ingestion_description.instance_id,
3532 remap_collection_id: ingestion_description.remap_collection_id,
3533 };
3534
3535 let storage_instance_id = description.instance_id;
3536 let instance = self
3538 .instances
3539 .get_mut(&storage_instance_id)
3540 .ok_or_else(|| StorageError::IngestionInstanceMissing {
3541 storage_instance_id,
3542 ingestion_id: id,
3543 })?;
3544
3545 let augmented_ingestion = Box::new(RunIngestionCommand { id, description });
3546 instance.send(StorageCommand::RunIngestion(augmented_ingestion));
3547
3548 Ok(())
3549 }
3550
3551 fn run_export(&mut self, id: GlobalId) -> Result<(), StorageError> {
3554 let DataSource::Sink { desc: description } = &self.collections[&id].data_source else {
3555 return Err(StorageError::IdentifierMissing(id));
3556 };
3557
3558 let from_storage_metadata = self
3559 .storage_collections
3560 .collection_metadata(description.sink.from)?;
3561 let to_storage_metadata = self.storage_collections.collection_metadata(id)?;
3562
3563 let export_state = self.storage_collections.collection_frontiers(id)?;
3567 let mut as_of = description.sink.as_of.clone();
3568 as_of.join_assign(&export_state.implied_capability);
3569 let with_snapshot = description.sink.with_snapshot
3570 && !PartialOrder::less_than(&as_of, &export_state.write_frontier);
3571
3572 info!(
3573 sink_id = %id,
3574 from_id = %description.sink.from,
3575 write_frontier = ?export_state.write_frontier,
3576 ?as_of,
3577 ?with_snapshot,
3578 "run_export"
3579 );
3580
3581 let cmd = RunSinkCommand {
3582 id,
3583 description: StorageSinkDesc {
3584 from: description.sink.from,
3585 from_desc: description.sink.from_desc.clone(),
3586 connection: description.sink.connection.clone(),
3587 envelope: description.sink.envelope,
3588 as_of,
3589 version: description.sink.version,
3590 from_storage_metadata,
3591 with_snapshot,
3592 to_storage_metadata,
3593 commit_interval: description.sink.commit_interval,
3594 },
3595 };
3596
3597 let storage_instance_id = description.instance_id.clone();
3598
3599 let instance = self
3600 .instances
3601 .get_mut(&storage_instance_id)
3602 .ok_or_else(|| StorageError::ExportInstanceMissing {
3603 storage_instance_id,
3604 export_id: id,
3605 })?;
3606
3607 instance.send(StorageCommand::RunSink(Box::new(cmd)));
3608
3609 Ok(())
3610 }
3611
3612 fn update_frontier_introspection(&mut self) {
3617 let mut global_frontiers = BTreeMap::new();
3618 let mut replica_frontiers = BTreeMap::new();
3619
3620 for collection_frontiers in self.storage_collections.active_collection_frontiers() {
3621 let id = collection_frontiers.id;
3622 let since = collection_frontiers.read_capabilities;
3623 let upper = collection_frontiers.write_frontier;
3624
3625 let instance = self
3626 .collections
3627 .get(&id)
3628 .and_then(|collection_state| match &collection_state.extra_state {
3629 CollectionStateExtra::Ingestion(ingestion) => Some(ingestion.instance_id),
3630 CollectionStateExtra::Export(export) => Some(export.cluster_id()),
3631 CollectionStateExtra::None => None,
3632 })
3633 .and_then(|i| self.instances.get(&i));
3634
3635 if let Some(instance) = instance {
3636 for replica_id in instance.replica_ids() {
3637 replica_frontiers.insert((id, replica_id), upper.clone());
3638 }
3639 }
3640
3641 global_frontiers.insert(id, (since, upper));
3642 }
3643
3644 let mut global_updates = Vec::new();
3645 let mut replica_updates = Vec::new();
3646
3647 let mut push_global_update =
3648 |id: GlobalId,
3649 (since, upper): (Antichain<Timestamp>, Antichain<Timestamp>),
3650 diff: Diff| {
3651 let read_frontier = since.into_option().map_or(Datum::Null, |t| t.into());
3652 let write_frontier = upper.into_option().map_or(Datum::Null, |t| t.into());
3653 let row = Row::pack_slice(&[
3654 Datum::String(&id.to_string()),
3655 read_frontier,
3656 write_frontier,
3657 ]);
3658 global_updates.push((row, diff));
3659 };
3660
3661 let mut push_replica_update =
3662 |(id, replica_id): (GlobalId, ReplicaId), upper: Antichain<Timestamp>, diff: Diff| {
3663 let write_frontier = upper.into_option().map_or(Datum::Null, |t| t.into());
3664 let row = Row::pack_slice(&[
3665 Datum::String(&id.to_string()),
3666 Datum::String(&replica_id.to_string()),
3667 write_frontier,
3668 ]);
3669 replica_updates.push((row, diff));
3670 };
3671
3672 let mut old_global_frontiers =
3673 std::mem::replace(&mut self.recorded_frontiers, global_frontiers);
3674 for (&id, new) in &self.recorded_frontiers {
3675 match old_global_frontiers.remove(&id) {
3676 Some(old) if &old != new => {
3677 push_global_update(id, new.clone(), Diff::ONE);
3678 push_global_update(id, old, Diff::MINUS_ONE);
3679 }
3680 Some(_) => (),
3681 None => push_global_update(id, new.clone(), Diff::ONE),
3682 }
3683 }
3684 for (id, old) in old_global_frontiers {
3685 push_global_update(id, old, Diff::MINUS_ONE);
3686 }
3687
3688 let mut old_replica_frontiers =
3689 std::mem::replace(&mut self.recorded_replica_frontiers, replica_frontiers);
3690 for (&key, new) in &self.recorded_replica_frontiers {
3691 match old_replica_frontiers.remove(&key) {
3692 Some(old) if &old != new => {
3693 push_replica_update(key, new.clone(), Diff::ONE);
3694 push_replica_update(key, old, Diff::MINUS_ONE);
3695 }
3696 Some(_) => (),
3697 None => push_replica_update(key, new.clone(), Diff::ONE),
3698 }
3699 }
3700 for (key, old) in old_replica_frontiers {
3701 push_replica_update(key, old, Diff::MINUS_ONE);
3702 }
3703
3704 let id = self.introspection_ids[&IntrospectionType::Frontiers];
3705 self.collection_manager
3706 .differential_append(id, global_updates);
3707
3708 let id = self.introspection_ids[&IntrospectionType::ReplicaFrontiers];
3709 self.collection_manager
3710 .differential_append(id, replica_updates);
3711 }
3712
3713 fn refresh_wallclock_lag(&mut self) {
3732 let now_ms = (self.now)();
3733 let histogram_period =
3734 WallclockLagHistogramPeriod::from_epoch_millis(now_ms, self.config.config_set());
3735
3736 let frontier_lag = |frontier: &Antichain<Timestamp>| match frontier.as_option() {
3737 Some(ts) => (self.wallclock_lag)(*ts),
3738 None => Duration::ZERO,
3739 };
3740
3741 for frontiers in self.storage_collections.active_collection_frontiers() {
3742 let id = frontiers.id;
3743 let Some(collection) = self.collections.get_mut(&id) else {
3744 continue;
3745 };
3746
3747 let collection_unreadable =
3748 PartialOrder::less_equal(&frontiers.write_frontier, &frontiers.read_capabilities);
3749 let lag = if collection_unreadable {
3750 WallclockLag::Undefined
3751 } else {
3752 let lag = frontier_lag(&frontiers.write_frontier);
3753 WallclockLag::Seconds(lag.as_secs())
3754 };
3755
3756 collection.wallclock_lag_max = collection.wallclock_lag_max.max(lag);
3757
3758 let secs = lag.unwrap_seconds_or(u64::MAX);
3761 collection.wallclock_lag_metrics.observe(secs);
3762
3763 if let Some(stash) = &mut collection.wallclock_lag_histogram_stash {
3764 let bucket = lag.map_seconds(|secs| secs.next_power_of_two());
3765
3766 let instance_id = match &collection.extra_state {
3767 CollectionStateExtra::Ingestion(i) => Some(i.instance_id),
3768 CollectionStateExtra::Export(e) => Some(e.cluster_id()),
3769 CollectionStateExtra::None => None,
3770 };
3771 let workload_class = instance_id
3772 .and_then(|id| self.instances.get(&id))
3773 .and_then(|i| i.workload_class.clone());
3774 let labels = match workload_class {
3775 Some(wc) => [("workload_class", wc.clone())].into(),
3776 None => BTreeMap::new(),
3777 };
3778
3779 let key = (histogram_period, bucket, labels);
3780 *stash.entry(key).or_default() += Diff::ONE;
3781 }
3782 }
3783
3784 self.maybe_record_wallclock_lag();
3786 }
3787
3788 fn maybe_record_wallclock_lag(&mut self) {
3796 if self.read_only {
3797 return;
3798 }
3799
3800 let duration_trunc = |datetime: DateTime<_>, interval| {
3801 let td = TimeDelta::from_std(interval).ok()?;
3802 datetime.duration_trunc(td).ok()
3803 };
3804
3805 let interval = WALLCLOCK_LAG_RECORDING_INTERVAL.get(self.config.config_set());
3806 let now_dt = mz_ore::now::to_datetime((self.now)());
3807 let now_trunc = duration_trunc(now_dt, interval).unwrap_or_else(|| {
3808 soft_panic_or_log!("excessive wallclock lag recording interval: {interval:?}");
3809 let default = WALLCLOCK_LAG_RECORDING_INTERVAL.default();
3810 duration_trunc(now_dt, *default).unwrap()
3811 });
3812 if now_trunc <= self.wallclock_lag_last_recorded {
3813 return;
3814 }
3815
3816 let now_ts: CheckedTimestamp<_> = now_trunc.try_into().expect("must fit");
3817
3818 let mut history_updates = Vec::new();
3819 let mut histogram_updates = Vec::new();
3820 let mut row_buf = Row::default();
3821 for frontiers in self.storage_collections.active_collection_frontiers() {
3822 let id = frontiers.id;
3823 let Some(collection) = self.collections.get_mut(&id) else {
3824 continue;
3825 };
3826
3827 let max_lag = std::mem::replace(&mut collection.wallclock_lag_max, WallclockLag::MIN);
3828 let row = Row::pack_slice(&[
3829 Datum::String(&id.to_string()),
3830 Datum::Null,
3831 max_lag.into_interval_datum(),
3832 Datum::TimestampTz(now_ts),
3833 ]);
3834 history_updates.push((row, Diff::ONE));
3835
3836 let Some(stash) = &mut collection.wallclock_lag_histogram_stash else {
3837 continue;
3838 };
3839
3840 for ((period, lag, labels), count) in std::mem::take(stash) {
3841 let mut packer = row_buf.packer();
3842 packer.extend([
3843 Datum::TimestampTz(period.start),
3844 Datum::TimestampTz(period.end),
3845 Datum::String(&id.to_string()),
3846 lag.into_uint64_datum(),
3847 ]);
3848 let labels = labels.iter().map(|(k, v)| (*k, Datum::String(v)));
3849 packer.push_dict(labels);
3850
3851 histogram_updates.push((row_buf.clone(), count));
3852 }
3853 }
3854
3855 if !history_updates.is_empty() {
3856 self.append_introspection_updates(
3857 IntrospectionType::WallclockLagHistory,
3858 history_updates,
3859 );
3860 }
3861 if !histogram_updates.is_empty() {
3862 self.append_introspection_updates(
3863 IntrospectionType::WallclockLagHistogram,
3864 histogram_updates,
3865 );
3866 }
3867
3868 self.wallclock_lag_last_recorded = now_trunc;
3869 }
3870
3871 fn maintain(&mut self) {
3876 self.update_frontier_introspection();
3877 self.refresh_wallclock_lag();
3878
3879 for instance in self.instances.values_mut() {
3881 instance.refresh_state_metrics();
3882 }
3883 }
3884}
3885
3886impl From<&IntrospectionType> for CollectionManagerKind {
3887 fn from(value: &IntrospectionType) -> Self {
3888 match value {
3889 IntrospectionType::ShardMapping
3890 | IntrospectionType::Frontiers
3891 | IntrospectionType::ReplicaFrontiers
3892 | IntrospectionType::StorageSourceStatistics
3893 | IntrospectionType::StorageSinkStatistics
3894 | IntrospectionType::ComputeDependencies
3895 | IntrospectionType::ComputeOperatorHydrationStatus
3896 | IntrospectionType::ComputeMaterializedViewRefreshes
3897 | IntrospectionType::ComputeErrorCounts
3898 | IntrospectionType::ComputeHydrationTimes
3899 | IntrospectionType::ComputeObjectArrangementSizes => {
3900 CollectionManagerKind::Differential
3901 }
3902
3903 IntrospectionType::SourceStatusHistory
3904 | IntrospectionType::SinkStatusHistory
3905 | IntrospectionType::PrivatelinkConnectionStatusHistory
3906 | IntrospectionType::ReplicaStatusHistory
3907 | IntrospectionType::ReplicaMetricsHistory
3908 | IntrospectionType::WallclockLagHistory
3909 | IntrospectionType::WallclockLagHistogram
3910 | IntrospectionType::PreparedStatementHistory
3911 | IntrospectionType::StatementExecutionHistory
3912 | IntrospectionType::SessionHistory
3913 | IntrospectionType::StatementLifecycleHistory
3914 | IntrospectionType::SqlText => CollectionManagerKind::AppendOnly,
3915 }
3916 }
3917}
3918
3919async fn snapshot_statistics(
3925 id: GlobalId,
3926 upper: Antichain<Timestamp>,
3927 storage_collections: &Arc<dyn StorageCollections + Send + Sync>,
3928) -> Vec<Row> {
3929 match upper.as_option() {
3930 Some(f) if f > &Timestamp::MIN => {
3931 let as_of = f.step_back().unwrap();
3932
3933 let snapshot = storage_collections.snapshot(id, as_of).await.unwrap();
3934 snapshot
3935 .into_iter()
3936 .map(|(row, diff)| {
3937 assert_eq!(diff, 1);
3938 row
3939 })
3940 .collect()
3941 }
3942 _ => Vec::new(),
3945 }
3946}
3947
3948async fn read_handle_for_snapshot(
3949 persist: &PersistClientCache,
3950 id: GlobalId,
3951 metadata: &CollectionMetadata,
3952) -> Result<ReadHandle<SourceData, (), Timestamp, StorageDiff>, StorageError> {
3953 let persist_client = persist
3954 .open(metadata.persist_location.clone())
3955 .await
3956 .unwrap();
3957
3958 let read_handle = persist_client
3963 .open_leased_reader::<SourceData, (), _, _>(
3964 metadata.data_shard,
3965 Arc::new(metadata.relation_desc.clone()),
3966 Arc::new(UnitSchema),
3967 Diagnostics {
3968 shard_name: id.to_string(),
3969 handle_purpose: format!("snapshot {}", id),
3970 },
3971 USE_CRITICAL_SINCE_SNAPSHOT.get(persist_client.dyncfgs()),
3972 )
3973 .await
3974 .expect("invalid persist usage");
3975 Ok(read_handle)
3976}
3977
3978#[derive(Debug)]
3980struct CollectionState {
3981 pub data_source: DataSource,
3983
3984 pub collection_metadata: CollectionMetadata,
3985
3986 pub extra_state: CollectionStateExtra,
3987
3988 wallclock_lag_max: WallclockLag,
3990 wallclock_lag_histogram_stash: Option<
3997 BTreeMap<
3998 (
3999 WallclockLagHistogramPeriod,
4000 WallclockLag,
4001 BTreeMap<&'static str, String>,
4002 ),
4003 Diff,
4004 >,
4005 >,
4006 wallclock_lag_metrics: WallclockLagMetrics,
4008}
4009
4010impl CollectionState {
4011 fn new(
4012 data_source: DataSource,
4013 collection_metadata: CollectionMetadata,
4014 extra_state: CollectionStateExtra,
4015 wallclock_lag_metrics: WallclockLagMetrics,
4016 ) -> Self {
4017 let wallclock_lag_histogram_stash = match &data_source {
4021 DataSource::Other => None,
4022 _ => Some(Default::default()),
4023 };
4024
4025 Self {
4026 data_source,
4027 collection_metadata,
4028 extra_state,
4029 wallclock_lag_max: WallclockLag::MIN,
4030 wallclock_lag_histogram_stash,
4031 wallclock_lag_metrics,
4032 }
4033 }
4034}
4035
4036#[derive(Debug)]
4038enum CollectionStateExtra {
4039 Ingestion(IngestionState),
4040 Export(ExportState),
4041 None,
4042}
4043
4044#[derive(Debug)]
4046struct IngestionState {
4047 pub read_capabilities: MutableAntichain<Timestamp>,
4049
4050 pub derived_since: Antichain<Timestamp>,
4053
4054 pub dependency_read_holds: Vec<ReadHold>,
4056
4057 pub write_frontier: Antichain<Timestamp>,
4059
4060 pub hold_policy: ReadPolicy,
4067
4068 pub instance_id: StorageInstanceId,
4070
4071 pub hydrated_on: BTreeSet<ReplicaId>,
4073}
4074
4075struct StatusHistoryDesc<K> {
4080 retention_policy: StatusHistoryRetentionPolicy,
4081 extract_key: Box<dyn Fn(&[Datum]) -> K + Send>,
4082 extract_time: Box<dyn Fn(&[Datum]) -> CheckedTimestamp<DateTime<Utc>> + Send>,
4083}
4084enum StatusHistoryRetentionPolicy {
4085 LastN(usize),
4087 TimeWindow(Duration),
4089}
4090
4091fn source_status_history_desc(
4092 params: &StorageParameters,
4093) -> StatusHistoryDesc<(GlobalId, Option<ReplicaId>)> {
4094 let desc = &MZ_SOURCE_STATUS_HISTORY_DESC;
4095 let (source_id_idx, _) = desc.get_by_name(&"source_id".into()).expect("exists");
4096 let (replica_id_idx, _) = desc.get_by_name(&"replica_id".into()).expect("exists");
4097 let (time_idx, _) = desc.get_by_name(&"occurred_at".into()).expect("exists");
4098
4099 StatusHistoryDesc {
4100 retention_policy: StatusHistoryRetentionPolicy::LastN(
4101 params.keep_n_source_status_history_entries,
4102 ),
4103 extract_key: Box::new(move |datums| {
4104 (
4105 GlobalId::from_str(datums[source_id_idx].unwrap_str()).expect("GlobalId column"),
4106 if datums[replica_id_idx].is_null() {
4107 None
4108 } else {
4109 Some(
4110 ReplicaId::from_str(datums[replica_id_idx].unwrap_str())
4111 .expect("ReplicaId column"),
4112 )
4113 },
4114 )
4115 }),
4116 extract_time: Box::new(move |datums| datums[time_idx].unwrap_timestamptz()),
4117 }
4118}
4119
4120fn sink_status_history_desc(
4121 params: &StorageParameters,
4122) -> StatusHistoryDesc<(GlobalId, Option<ReplicaId>)> {
4123 let desc = &MZ_SINK_STATUS_HISTORY_DESC;
4124 let (sink_id_idx, _) = desc.get_by_name(&"sink_id".into()).expect("exists");
4125 let (replica_id_idx, _) = desc.get_by_name(&"replica_id".into()).expect("exists");
4126 let (time_idx, _) = desc.get_by_name(&"occurred_at".into()).expect("exists");
4127
4128 StatusHistoryDesc {
4129 retention_policy: StatusHistoryRetentionPolicy::LastN(
4130 params.keep_n_sink_status_history_entries,
4131 ),
4132 extract_key: Box::new(move |datums| {
4133 (
4134 GlobalId::from_str(datums[sink_id_idx].unwrap_str()).expect("GlobalId column"),
4135 if datums[replica_id_idx].is_null() {
4136 None
4137 } else {
4138 Some(
4139 ReplicaId::from_str(datums[replica_id_idx].unwrap_str())
4140 .expect("ReplicaId column"),
4141 )
4142 },
4143 )
4144 }),
4145 extract_time: Box::new(move |datums| datums[time_idx].unwrap_timestamptz()),
4146 }
4147}
4148
4149fn privatelink_status_history_desc(params: &StorageParameters) -> StatusHistoryDesc<GlobalId> {
4150 let desc = &MZ_AWS_PRIVATELINK_CONNECTION_STATUS_HISTORY_DESC;
4151 let (key_idx, _) = desc.get_by_name(&"connection_id".into()).expect("exists");
4152 let (time_idx, _) = desc.get_by_name(&"occurred_at".into()).expect("exists");
4153
4154 StatusHistoryDesc {
4155 retention_policy: StatusHistoryRetentionPolicy::LastN(
4156 params.keep_n_privatelink_status_history_entries,
4157 ),
4158 extract_key: Box::new(move |datums| {
4159 GlobalId::from_str(datums[key_idx].unwrap_str()).expect("GlobalId column")
4160 }),
4161 extract_time: Box::new(move |datums| datums[time_idx].unwrap_timestamptz()),
4162 }
4163}
4164
4165fn replica_status_history_desc(params: &StorageParameters) -> StatusHistoryDesc<(GlobalId, u64)> {
4166 let desc = &REPLICA_STATUS_HISTORY_DESC;
4167 let (replica_idx, _) = desc.get_by_name(&"replica_id".into()).expect("exists");
4168 let (process_idx, _) = desc.get_by_name(&"process_id".into()).expect("exists");
4169 let (time_idx, _) = desc.get_by_name(&"occurred_at".into()).expect("exists");
4170
4171 StatusHistoryDesc {
4172 retention_policy: StatusHistoryRetentionPolicy::TimeWindow(
4173 params.replica_status_history_retention_window,
4174 ),
4175 extract_key: Box::new(move |datums| {
4176 (
4177 GlobalId::from_str(datums[replica_idx].unwrap_str()).expect("GlobalId column"),
4178 datums[process_idx].unwrap_uint64(),
4179 )
4180 }),
4181 extract_time: Box::new(move |datums| datums[time_idx].unwrap_timestamptz()),
4182 }
4183}
4184
4185fn swap_updates(
4187 from: &mut Antichain<Timestamp>,
4188 mut replace_with: Antichain<Timestamp>,
4189) -> ChangeBatch<Timestamp> {
4190 let mut update = ChangeBatch::new();
4191 if PartialOrder::less_equal(from, &replace_with) {
4192 update.extend(replace_with.iter().map(|time| (*time, 1)));
4193 std::mem::swap(from, &mut replace_with);
4194 update.extend(replace_with.iter().map(|time| (*time, -1)));
4195 }
4196 update
4197}