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