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_ore::collections::CollectionExt;
39use mz_ore::metrics::MetricsRegistry;
40use mz_ore::now::NowFn;
41use mz_ore::task::AbortOnDropHandle;
42use mz_ore::{assert_none, halt, instrument, soft_panic_or_log};
43use mz_persist_client::batch::ProtoBatch;
44use mz_persist_client::cache::PersistClientCache;
45use mz_persist_client::cfg::USE_CRITICAL_SINCE_SNAPSHOT;
46use mz_persist_client::critical::Opaque;
47use mz_persist_client::read::ReadHandle;
48use mz_persist_client::schema::CaESchema;
49use mz_persist_client::write::WriteHandle;
50use mz_persist_client::{Diagnostics, PersistClient, PersistLocation, ShardId};
51use mz_persist_types::codec_impls::UnitSchema;
52use mz_repr::adt::timestamp::CheckedTimestamp;
53use mz_repr::{Datum, Diff, GlobalId, RelationDesc, RelationVersion, Row, Timestamp};
54use mz_storage_client::client::{
55 AppendOnlyUpdate, RunIngestionCommand, RunOneshotIngestion, RunSinkCommand, Status,
56 StatusUpdate, StorageCommand, StorageResponse, TableData,
57};
58use mz_storage_client::controller::{
59 BoxFuture, CollectionDescription, DataSource, ExportDescription, ExportState,
60 IntrospectionType, MonotonicAppender, PersistEpoch, Response, StorageController,
61 StorageMetadata, StorageTxn, StorageWriteOp, TableRegistration, WallclockLag,
62 WallclockLagHistogramPeriod,
63};
64use mz_storage_client::healthcheck::{
65 MZ_AWS_PRIVATELINK_CONNECTION_STATUS_HISTORY_DESC, MZ_SINK_STATUS_HISTORY_DESC,
66 MZ_SOURCE_STATUS_HISTORY_DESC, REPLICA_STATUS_HISTORY_DESC,
67};
68use mz_storage_client::metrics::StorageControllerMetrics;
69use mz_storage_client::statistics::{
70 ControllerSinkStatistics, ControllerSourceStatistics, WebhookStatistics,
71};
72use mz_storage_client::storage_collections::StorageCollections;
73use mz_storage_types::configuration::StorageConfiguration;
74use mz_storage_types::connections::ConnectionContext;
75use mz_storage_types::connections::inline::InlinedConnection;
76use mz_storage_types::controller::{AlterError, CollectionMetadata, StorageError, TxnsCodecRow};
77use mz_storage_types::errors::CollectionMissing;
78use mz_storage_types::instances::StorageInstanceId;
79use mz_storage_types::oneshot_sources::{OneshotIngestionRequest, OneshotResultCallback};
80use mz_storage_types::parameters::StorageParameters;
81use mz_storage_types::read_holds::ReadHold;
82use mz_storage_types::read_policy::ReadPolicy;
83use mz_storage_types::sinks::{StorageSinkConnection, StorageSinkDesc};
84use mz_storage_types::sources::{
85 GenericSourceConnection, IngestionDescription, SourceConnection, SourceData, SourceDesc,
86 SourceExport, SourceExportDataConfig,
87};
88use mz_storage_types::{AlterCompatible, StorageDiff, dyncfgs};
89use mz_txn_wal::metrics::Metrics as TxnMetrics;
90use mz_txn_wal::txn_read::TxnsRead;
91use mz_txn_wal::txns::TxnsHandle;
92use timely::order::PartialOrder;
93use timely::progress::frontier::MutableAntichain;
94use timely::progress::{Antichain, ChangeBatch};
95use tokio::sync::watch::{Sender, channel};
96use tokio::sync::{mpsc, oneshot};
97use tokio::time::MissedTickBehavior;
98use tokio::time::error::Elapsed;
99use tracing::{debug, info, warn};
100
101mod collection_mgmt;
102mod history;
103mod instance;
104mod persist_handles;
105mod rtr;
106mod statistics;
107
108#[derive(Derivative)]
109#[derivative(Debug)]
110struct PendingOneshotIngestion {
111 #[derivative(Debug = "ignore")]
113 result_tx: OneshotResultCallback<ProtoBatch>,
114 cluster_id: StorageInstanceId,
116}
117
118impl PendingOneshotIngestion {
119 pub(crate) fn cancel(self) {
123 (self.result_tx)(vec![Err("canceled".to_string())])
124 }
125}
126
127#[derive(Derivative)]
129#[derivative(Debug)]
130pub struct Controller {
131 build_info: &'static BuildInfo,
133 now: NowFn,
135
136 read_only: bool,
142
143 pub(crate) collections: BTreeMap<GlobalId, CollectionState>,
148
149 dropped_objects: BTreeMap<GlobalId, BTreeSet<ReplicaId>>,
158
159 pub(crate) persist_table_worker: persist_handles::PersistTableWriteWorker,
161 txns_read: TxnsRead<Timestamp>,
163 txns_metrics: Arc<TxnMetrics>,
164 stashed_responses: Vec<(Option<ReplicaId>, StorageResponse)>,
165 #[derivative(Debug = "ignore")]
167 pending_table_handle_drops_tx: mpsc::UnboundedSender<GlobalId>,
168 #[derivative(Debug = "ignore")]
170 pending_table_handle_drops_rx: mpsc::UnboundedReceiver<GlobalId>,
171 #[derivative(Debug = "ignore")]
173 pending_oneshot_ingestions: BTreeMap<uuid::Uuid, PendingOneshotIngestion>,
174
175 pub(crate) collection_manager: collection_mgmt::CollectionManager,
177
178 pub(crate) introspection_ids: BTreeMap<IntrospectionType, GlobalId>,
180 introspection_tokens: Arc<Mutex<BTreeMap<GlobalId, Box<dyn Any + Send + Sync>>>>,
185
186 source_statistics: Arc<Mutex<statistics::SourceStatistics>>,
191 sink_statistics: Arc<Mutex<BTreeMap<(GlobalId, Option<ReplicaId>), ControllerSinkStatistics>>>,
194 statistics_interval_sender: Sender<Duration>,
196
197 instances: BTreeMap<StorageInstanceId, Instance>,
199 initialized: bool,
201 config: StorageConfiguration,
203 persist_location: PersistLocation,
205 persist: Arc<PersistClientCache>,
207 metrics: StorageControllerMetrics,
209 recorded_frontiers: BTreeMap<GlobalId, (Antichain<Timestamp>, Antichain<Timestamp>)>,
212 recorded_replica_frontiers: BTreeMap<(GlobalId, ReplicaId), Antichain<Timestamp>>,
215
216 #[derivative(Debug = "ignore")]
218 wallclock_lag: WallclockLagFn<Timestamp>,
219 wallclock_lag_last_recorded: DateTime<Utc>,
221
222 storage_collections: Arc<dyn StorageCollections + Send + Sync>,
224 migrated_storage_collections: BTreeSet<GlobalId>,
226
227 maintenance_ticker: tokio::time::Interval,
229 maintenance_scheduled: bool,
231
232 instance_response_tx: mpsc::UnboundedSender<(Option<ReplicaId>, StorageResponse)>,
234 instance_response_rx: mpsc::UnboundedReceiver<(Option<ReplicaId>, StorageResponse)>,
236
237 persist_warm_task: Option<AbortOnDropHandle<Box<dyn Debug + Send>>>,
239}
240
241enum WriteHandleOrUpper {
250 Handle(WriteHandle<SourceData, (), Timestamp, StorageDiff>),
251 Upper(Antichain<Timestamp>),
252}
253
254impl WriteHandleOrUpper {
255 fn upper(&self) -> Antichain<Timestamp> {
256 match self {
257 Self::Handle(handle) => handle.upper().clone(),
258 Self::Upper(upper) => upper.clone(),
259 }
260 }
261
262 fn expect_handle(self, context: &str) -> WriteHandle<SourceData, (), Timestamp, StorageDiff> {
265 match self {
266 Self::Handle(handle) => handle,
267 Self::Upper(_) => panic!("write handle required: {context}"),
268 }
269 }
270}
271
272fn warm_persist_state_in_background(
273 client: PersistClient,
274 shard_ids: impl Iterator<Item = ShardId> + Send + 'static,
275) -> mz_ore::task::JoinHandle<Box<dyn Debug + Send>> {
276 const MAX_CONCURRENT_WARMS: usize = 16;
278 let logic = async move {
279 let fetchers: Vec<_> = tokio_stream::iter(shard_ids)
280 .map(|shard_id| {
281 let client = client.clone();
282 async move {
283 client
284 .create_batch_fetcher::<SourceData, (), mz_repr::Timestamp, StorageDiff>(
285 shard_id,
286 Arc::new(RelationDesc::empty()),
287 Arc::new(UnitSchema),
288 true,
289 Diagnostics::from_purpose("warm persist load state"),
290 )
291 .await
292 }
293 })
294 .buffer_unordered(MAX_CONCURRENT_WARMS)
295 .collect()
296 .await;
297 let fetchers: Box<dyn Debug + Send> = Box::new(fetchers);
298 fetchers
299 };
300 mz_ore::task::spawn(|| "warm_persist_load_state", logic)
301}
302
303#[async_trait(?Send)]
304impl StorageController for Controller {
305 fn initialization_complete(&mut self) {
306 self.reconcile_dangling_statistics();
307 self.initialized = true;
308
309 for instance in self.instances.values_mut() {
310 instance.send(StorageCommand::InitializationComplete);
311 }
312 }
313
314 fn update_parameters(&mut self, config_params: StorageParameters) {
315 self.storage_collections
316 .update_parameters(config_params.clone());
317
318 self.persist.cfg().apply_from(&config_params.dyncfg_updates);
321
322 for instance in self.instances.values_mut() {
323 let params = Box::new(config_params.clone());
324 instance.send(StorageCommand::UpdateConfiguration(params));
325 }
326 self.config.update(config_params);
327 self.statistics_interval_sender
328 .send_replace(self.config.parameters.statistics_interval);
329 self.collection_manager.update_user_batch_duration(
330 self.config
331 .parameters
332 .user_storage_managed_collections_batch_duration,
333 );
334 }
335
336 fn config(&self) -> &StorageConfiguration {
338 &self.config
339 }
340
341 fn collection_metadata(&self, id: GlobalId) -> Result<CollectionMetadata, CollectionMissing> {
342 self.storage_collections.collection_metadata(id)
343 }
344
345 fn collection_hydrated(&self, collection_id: GlobalId) -> Result<bool, StorageError> {
346 let collection = self.collection(collection_id)?;
347
348 let instance_id = match &collection.data_source {
349 DataSource::Ingestion(ingestion_description) => ingestion_description.instance_id,
350 DataSource::IngestionExport { ingestion_id, .. } => {
351 let ingestion_state = self.collections.get(ingestion_id).expect("known to exist");
352
353 let instance_id = match &ingestion_state.data_source {
354 DataSource::Ingestion(ingestion_desc) => ingestion_desc.instance_id,
355 _ => unreachable!("SourceExport must only refer to primary source"),
356 };
357
358 instance_id
359 }
360 _ => return Ok(true),
361 };
362
363 let instance = self.instances.get(&instance_id).ok_or_else(|| {
364 StorageError::IngestionInstanceMissing {
365 storage_instance_id: instance_id,
366 ingestion_id: collection_id,
367 }
368 })?;
369
370 if instance.replica_ids().next().is_none() {
371 return Ok(true);
374 }
375
376 match &collection.extra_state {
377 CollectionStateExtra::Ingestion(ingestion_state) => {
378 Ok(ingestion_state.hydrated_on.len() >= 1)
380 }
381 CollectionStateExtra::Export(_) => {
382 Ok(true)
387 }
388 CollectionStateExtra::None => {
389 Ok(true)
393 }
394 }
395 }
396
397 #[mz_ore::instrument(level = "debug")]
398 fn collections_hydrated_on_replicas(
399 &self,
400 target_replica_ids: Option<Vec<ReplicaId>>,
401 target_cluster_id: &StorageInstanceId,
402 exclude_collections: &BTreeSet<GlobalId>,
403 ) -> Result<bool, StorageError> {
404 if target_replica_ids.as_ref().is_some_and(|v| v.is_empty()) {
407 return Ok(true);
408 }
409
410 let target_replicas: Option<BTreeSet<ReplicaId>> =
413 target_replica_ids.map(|ids| ids.into_iter().collect());
414
415 let instance = self.instances.get(target_cluster_id);
416
417 let mut all_hydrated = true;
418 for (collection_id, collection_state) in self.collections.iter() {
419 if collection_id.is_transient() || exclude_collections.contains(collection_id) {
420 continue;
421 }
422 let hydrated = match &collection_state.extra_state {
423 CollectionStateExtra::Ingestion(state) => {
424 if &state.instance_id != target_cluster_id {
425 continue;
426 }
427 match &target_replicas {
428 Some(target_replicas) => {
429 let scheduled_on = instance
437 .map(|i| i.get_active_replicas_for_object(collection_id))
438 .unwrap_or_default();
439 if scheduled_on.is_disjoint(target_replicas) {
440 true
441 } else {
442 !state.hydrated_on.is_disjoint(target_replicas)
443 }
444 }
445 None => {
446 state.hydrated_on.len() >= 1
449 }
450 }
451 }
452 CollectionStateExtra::Export(_) => {
453 true
458 }
459 CollectionStateExtra::None => {
460 true
464 }
465 };
466 if !hydrated {
467 tracing::info!(%collection_id, "collection is not hydrated on any replica");
468 all_hydrated = false;
469 }
472 }
473 Ok(all_hydrated)
474 }
475
476 fn collection_frontiers(
477 &self,
478 id: GlobalId,
479 ) -> Result<(Antichain<Timestamp>, Antichain<Timestamp>), CollectionMissing> {
480 let frontiers = self.storage_collections.collection_frontiers(id)?;
481 Ok((frontiers.implied_capability, frontiers.write_frontier))
482 }
483
484 fn collections_frontiers(
485 &self,
486 mut ids: Vec<GlobalId>,
487 ) -> Result<Vec<(GlobalId, Antichain<Timestamp>, Antichain<Timestamp>)>, CollectionMissing>
488 {
489 let mut result = vec![];
490 ids.retain(|&id| match self.export(id) {
495 Ok(export) => {
496 result.push((
497 id,
498 export.input_hold().since().clone(),
499 export.write_frontier.clone(),
500 ));
501 false
502 }
503 Err(_) => true,
504 });
505 result.extend(
506 self.storage_collections
507 .collections_frontiers(ids)?
508 .into_iter()
509 .map(|frontiers| {
510 (
511 frontiers.id,
512 frontiers.implied_capability,
513 frontiers.write_frontier,
514 )
515 }),
516 );
517
518 Ok(result)
519 }
520
521 fn active_collection_metadatas(&self) -> Vec<(GlobalId, CollectionMetadata)> {
522 self.storage_collections.active_collection_metadatas()
523 }
524
525 fn active_ingestion_exports(
526 &self,
527 instance_id: StorageInstanceId,
528 ) -> Box<dyn Iterator<Item = &GlobalId> + '_> {
529 let active_storage_collections: BTreeMap<_, _> = self
530 .storage_collections
531 .active_collection_frontiers()
532 .into_iter()
533 .map(|c| (c.id, c))
534 .collect();
535
536 let active_exports = self.instances[&instance_id]
537 .active_ingestion_exports()
538 .filter(move |id| {
539 let frontiers = active_storage_collections.get(id);
540 match frontiers {
541 Some(frontiers) => !frontiers.write_frontier.is_empty(),
542 None => {
543 false
545 }
546 }
547 });
548
549 Box::new(active_exports)
550 }
551
552 fn check_exists(&self, id: GlobalId) -> Result<(), StorageError> {
553 self.storage_collections.check_exists(id)
554 }
555
556 fn create_instance(&mut self, id: StorageInstanceId, workload_class: Option<String>) {
557 let metrics = self.metrics.for_instance(id);
558 let mut instance = Instance::new(
559 workload_class,
560 metrics,
561 self.now.clone(),
562 self.instance_response_tx.clone(),
563 );
564 if self.initialized {
565 instance.send(StorageCommand::InitializationComplete);
566 }
567 if !self.read_only {
568 instance.send(StorageCommand::AllowWrites);
569 }
570
571 let params = Box::new(self.config.parameters.clone());
572 instance.send(StorageCommand::UpdateConfiguration(params));
573
574 let old_instance = self.instances.insert(id, instance);
575 assert_none!(old_instance, "storage instance {id} already exists");
576 }
577
578 fn drop_instance(&mut self, id: StorageInstanceId) {
579 let instance = self.instances.remove(&id);
580 assert!(instance.is_some(), "storage instance {id} does not exist");
581 }
582
583 fn update_instance_workload_class(
584 &mut self,
585 id: StorageInstanceId,
586 workload_class: Option<String>,
587 ) {
588 let instance = self
589 .instances
590 .get_mut(&id)
591 .unwrap_or_else(|| panic!("instance {id} does not exist"));
592
593 instance.workload_class = workload_class;
594 }
595
596 fn connect_replica(
597 &mut self,
598 instance_id: StorageInstanceId,
599 replica_id: ReplicaId,
600 location: ClusterReplicaLocation,
601 ) {
602 let instance = self
603 .instances
604 .get_mut(&instance_id)
605 .unwrap_or_else(|| panic!("instance {instance_id} does not exist"));
606
607 let config = ReplicaConfig {
608 build_info: self.build_info,
609 location,
610 grpc_client: self.config.parameters.grpc_client.clone(),
611 };
612 instance.add_replica(replica_id, config);
613 }
614
615 fn drop_replica(&mut self, instance_id: StorageInstanceId, replica_id: ReplicaId) {
616 let instance = self
617 .instances
618 .get_mut(&instance_id)
619 .unwrap_or_else(|| panic!("instance {instance_id} does not exist"));
620
621 let status_now = mz_ore::now::to_datetime((self.now)());
622 let mut source_status_updates = vec![];
623 let mut sink_status_updates = vec![];
624
625 let make_update = |id, object_type| StatusUpdate {
628 id,
629 status: Status::Paused,
630 timestamp: status_now,
631 error: None,
632 hints: BTreeSet::from([format!(
633 "The replica running this {object_type} has been dropped"
634 )]),
635 namespaced_errors: Default::default(),
636 replica_id: Some(replica_id),
637 };
638
639 for ingestion_id in instance.active_ingestions() {
640 if let Some(active_replicas) = self.dropped_objects.get_mut(ingestion_id) {
641 active_replicas.remove(&replica_id);
642 if active_replicas.is_empty() {
643 self.dropped_objects.remove(ingestion_id);
644 }
645 }
646
647 let ingestion = self
648 .collections
649 .get_mut(ingestion_id)
650 .expect("instance contains unknown ingestion");
651
652 let ingestion_description = match &ingestion.data_source {
653 DataSource::Ingestion(ingestion_description) => ingestion_description.clone(),
654 _ => panic!(
655 "unexpected data source for ingestion: {:?}",
656 ingestion.data_source
657 ),
658 };
659
660 let old_style_ingestion = *ingestion_id != ingestion_description.remap_collection_id;
661 let subsource_ids = ingestion_description.collection_ids().filter(|id| {
662 let should_discard =
667 old_style_ingestion && id == &ingestion_description.remap_collection_id;
668 !should_discard
669 });
670 for id in subsource_ids {
671 source_status_updates.push(make_update(id, "source"));
672 }
673 }
674
675 for id in instance.active_exports() {
676 if let Some(active_replicas) = self.dropped_objects.get_mut(id) {
677 active_replicas.remove(&replica_id);
678 if active_replicas.is_empty() {
679 self.dropped_objects.remove(id);
680 }
681 }
682
683 sink_status_updates.push(make_update(*id, "sink"));
684 }
685
686 instance.drop_replica(replica_id);
687
688 if !self.read_only {
689 if !source_status_updates.is_empty() {
690 self.append_status_introspection_updates(
691 IntrospectionType::SourceStatusHistory,
692 source_status_updates,
693 );
694 }
695 if !sink_status_updates.is_empty() {
696 self.append_status_introspection_updates(
697 IntrospectionType::SinkStatusHistory,
698 sink_status_updates,
699 );
700 }
701 }
702 }
703
704 async fn evolve_nullability_for_bootstrap(
705 &mut self,
706 storage_metadata: &StorageMetadata,
707 collections: Vec<(GlobalId, RelationDesc)>,
708 ) -> Result<(), StorageError> {
709 let persist_client = self
710 .persist
711 .open(self.persist_location.clone())
712 .await
713 .unwrap();
714
715 for (global_id, relation_desc) in collections {
716 let shard_id = storage_metadata.get_collection_shard(global_id)?;
717 let diagnostics = Diagnostics {
718 shard_name: global_id.to_string(),
719 handle_purpose: "evolve nullability for bootstrap".to_string(),
720 };
721 let latest_schema = persist_client
722 .latest_schema::<SourceData, (), Timestamp, StorageDiff>(shard_id, diagnostics)
723 .await
724 .expect("invalid persist usage");
725 let Some((schema_id, current_schema, _)) = latest_schema else {
726 tracing::debug!(?global_id, "no schema registered");
727 continue;
728 };
729 tracing::debug!(?global_id, ?current_schema, new_schema = ?relation_desc, "migrating schema");
730
731 let diagnostics = Diagnostics {
732 shard_name: global_id.to_string(),
733 handle_purpose: "evolve nullability for bootstrap".to_string(),
734 };
735 let evolve_result = persist_client
736 .compare_and_evolve_schema::<SourceData, (), Timestamp, StorageDiff>(
737 shard_id,
738 schema_id,
739 &relation_desc,
740 &UnitSchema,
741 diagnostics,
742 )
743 .await
744 .expect("invalid persist usage");
745 match evolve_result {
746 CaESchema::Ok(_) => (),
747 CaESchema::ExpectedMismatch {
748 schema_id,
749 key,
750 val: _,
751 } => {
752 return Err(StorageError::PersistSchemaEvolveRace {
753 global_id,
754 shard_id,
755 schema_id,
756 relation_desc: key,
757 });
758 }
759 CaESchema::Incompatible => {
760 return Err(StorageError::PersistInvalidSchemaEvolve {
761 global_id,
762 shard_id,
763 });
764 }
765 };
766 }
767
768 Ok(())
769 }
770
771 #[instrument(name = "storage::create_collections")]
790 async fn create_collections_for_bootstrap(
791 &mut self,
792 storage_metadata: &StorageMetadata,
793 register_ts: Option<Timestamp>,
794 mut collections: Vec<(GlobalId, CollectionDescription)>,
795 migrated_storage_collections: &BTreeSet<GlobalId>,
796 ) -> Result<(), StorageError> {
797 self.migrated_storage_collections
798 .extend(migrated_storage_collections.iter().cloned());
799
800 self.storage_collections
801 .create_collections_for_bootstrap(
802 storage_metadata,
803 register_ts,
804 collections.clone(),
805 migrated_storage_collections,
806 )
807 .await?;
808
809 drop(self.persist_warm_task.take());
812
813 collections.sort_by_key(|(id, _)| *id);
818 collections.dedup();
819 for pos in 1..collections.len() {
820 if collections[pos - 1].0 == collections[pos].0 {
821 return Err(StorageError::CollectionIdReused(collections[pos].0));
822 }
823 }
824
825 let enriched_with_metadata = collections
827 .into_iter()
828 .map(|(id, description)| {
829 let data_shard = storage_metadata.get_collection_shard(id)?;
830
831 let txns_shard = description
834 .data_source
835 .in_txns()
836 .then(|| *self.txns_read.txns_id());
837
838 let metadata = CollectionMetadata {
839 persist_location: self.persist_location.clone(),
840 data_shard,
841 relation_desc: description.desc.clone(),
842 txns_shard,
843 };
844
845 Ok((id, description, metadata))
846 })
847 .collect_vec();
848
849 let persist_client = self
851 .persist
852 .open(self.persist_location.clone())
853 .await
854 .unwrap();
855 let persist_client = &persist_client;
856
857 use futures::stream::{StreamExt, TryStreamExt};
860 let this = &*self;
861 let mut to_register: Vec<_> = futures::stream::iter(enriched_with_metadata)
862 .map(|data: Result<_, StorageError>| {
863 async move {
864 let (id, description, metadata) = data?;
865
866 debug!("mapping GlobalId={} to shard ({})", id, metadata.data_shard);
869
870 let write = if matches!(description.data_source, DataSource::Table) {
875 let diagnostics = Diagnostics {
876 shard_name: id.to_string(),
877 handle_purpose: format!("controller data for {}", id),
878 };
879 let upper = persist_client
880 .recent_upper::<SourceData, (), Timestamp, StorageDiff>(
881 metadata.data_shard,
882 diagnostics,
883 )
884 .await
885 .expect("invalid persist usage");
886 WriteHandleOrUpper::Upper(upper)
887 } else {
888 let write = this
889 .open_data_handles(
890 &id,
891 metadata.data_shard,
892 metadata.relation_desc.clone(),
893 persist_client,
894 )
895 .await;
896 WriteHandleOrUpper::Handle(write)
897 };
898
899 Ok::<_, StorageError>((id, description, write, metadata))
900 }
901 })
902 .buffer_unordered(50)
904 .try_collect()
917 .await?;
918
919 let mut to_execute = BTreeSet::new();
922 let mut new_collections = BTreeSet::new();
927
928 to_register.sort_by_key(|(id, ..)| *id);
930
931 let (tables_to_register, collections_to_register): (Vec<_>, Vec<_>) = to_register
937 .into_iter()
938 .partition(|(_id, desc, ..)| desc.data_source == DataSource::Table);
939 let to_register = tables_to_register
940 .into_iter()
941 .rev()
942 .chain(collections_to_register.into_iter());
943
944 let mut new_webhook_statistic_entries = BTreeSet::new();
948
949 for (id, description, write, metadata) in to_register {
950 let is_in_txns = |id, metadata: &CollectionMetadata| {
951 metadata.txns_shard.is_some()
952 && !(self.read_only && migrated_storage_collections.contains(&id))
953 };
954
955 to_execute.insert(id);
956 new_collections.insert(id);
957
958 let write_frontier = write.upper();
959
960 let storage_dependencies = self.determine_collection_dependencies(id, &description)?;
962
963 let dependency_read_holds = self
964 .storage_collections
965 .acquire_read_holds(storage_dependencies)
966 .expect("can acquire read holds");
967
968 let mut dependency_since = Antichain::from_elem(Timestamp::MIN);
969 for read_hold in dependency_read_holds.iter() {
970 dependency_since.join_assign(read_hold.since());
971 }
972
973 let data_source = description.data_source;
974
975 if !dependency_read_holds.is_empty()
984 && !is_in_txns(id, &metadata)
985 && !matches!(&data_source, DataSource::Sink { .. })
986 {
987 if dependency_since.is_empty() {
993 halt!(
994 "dependency since frontier is empty while dependent upper \
995 is not empty (dependent id={id}, write_frontier={:?}, dependency_read_holds={:?}), \
996 this indicates concurrent deletion of a collection",
997 write_frontier,
998 dependency_read_holds,
999 );
1000 }
1001
1002 if description.primary.is_none() {
1020 mz_ore::soft_assert_or_log!(
1021 write_frontier.elements() == &[Timestamp::MIN]
1022 || write_frontier.is_empty()
1023 || PartialOrder::less_than(&dependency_since, &write_frontier),
1024 "dependency since has advanced past dependent ({id}) upper \n
1025 dependent ({id}): upper {:?} \n
1026 dependency since {:?} \n
1027 dependency read holds: {:?}",
1028 write_frontier,
1029 dependency_since,
1030 dependency_read_holds,
1031 );
1032 }
1033 }
1034
1035 let mut extra_state = CollectionStateExtra::None;
1037 let mut maybe_instance_id = None;
1038 match &data_source {
1039 DataSource::Introspection(typ) => {
1040 debug!(
1041 ?data_source, meta = ?metadata,
1042 "registering {id} with persist monotonic worker",
1043 );
1044 self.register_introspection_collection(
1050 id,
1051 *typ,
1052 write.expect_handle("introspection collections are not tables"),
1053 persist_client.clone(),
1054 )?;
1055 }
1056 DataSource::Webhook => {
1057 debug!(
1058 ?data_source, meta = ?metadata,
1059 "registering {id} with persist monotonic worker",
1060 );
1061 new_webhook_statistic_entries.insert(id);
1064 self.collection_manager.register_append_only_collection(
1070 id,
1071 write.expect_handle("webhook collections are not tables"),
1072 false,
1073 None,
1074 );
1075 }
1076 DataSource::IngestionExport {
1077 ingestion_id,
1078 details,
1079 data_config,
1080 } => {
1081 debug!(
1082 ?data_source, meta = ?metadata,
1083 "not registering {id} with a controller persist worker",
1084 );
1085 let ingestion_state = self
1087 .collections
1088 .get_mut(ingestion_id)
1089 .expect("known to exist");
1090
1091 let instance_id = match &mut ingestion_state.data_source {
1092 DataSource::Ingestion(ingestion_desc) => {
1093 ingestion_desc.source_exports.insert(
1094 id,
1095 SourceExport {
1096 storage_metadata: (),
1097 details: details.clone(),
1098 data_config: data_config.clone(),
1099 },
1100 );
1101
1102 ingestion_desc.instance_id
1107 }
1108 _ => unreachable!(
1109 "SourceExport must only refer to primary sources that already exist"
1110 ),
1111 };
1112
1113 to_execute.remove(&id);
1115 to_execute.insert(*ingestion_id);
1116
1117 let ingestion_state = IngestionState {
1118 read_capabilities: MutableAntichain::from(dependency_since.clone()),
1119 dependency_read_holds,
1120 derived_since: dependency_since,
1121 write_frontier: Antichain::from_elem(Timestamp::MIN),
1122 hold_policy: ReadPolicy::step_back(),
1123 instance_id,
1124 hydrated_on: BTreeSet::new(),
1125 };
1126
1127 extra_state = CollectionStateExtra::Ingestion(ingestion_state);
1128 maybe_instance_id = Some(instance_id);
1129 }
1130 DataSource::Table => {
1131 debug!(
1132 ?data_source, meta = ?metadata,
1133 "not registering {id} with the txns shard here; the caller does that \
1134 through the group committer",
1135 );
1136 }
1137 DataSource::Progress | DataSource::Other => {
1138 debug!(
1139 ?data_source, meta = ?metadata,
1140 "not registering {id} with a controller persist worker",
1141 );
1142 }
1143 DataSource::Ingestion(ingestion_desc) => {
1144 debug!(
1145 ?data_source, meta = ?metadata,
1146 "not registering {id} with a controller persist worker",
1147 );
1148
1149 let mut dependency_since = Antichain::from_elem(Timestamp::MIN);
1150 for read_hold in dependency_read_holds.iter() {
1151 dependency_since.join_assign(read_hold.since());
1152 }
1153
1154 let ingestion_state = IngestionState {
1155 read_capabilities: MutableAntichain::from(dependency_since.clone()),
1156 dependency_read_holds,
1157 derived_since: dependency_since,
1158 write_frontier: Antichain::from_elem(Timestamp::MIN),
1159 hold_policy: ReadPolicy::step_back(),
1160 instance_id: ingestion_desc.instance_id,
1161 hydrated_on: BTreeSet::new(),
1162 };
1163
1164 extra_state = CollectionStateExtra::Ingestion(ingestion_state);
1165 maybe_instance_id = Some(ingestion_desc.instance_id);
1166 }
1167 DataSource::Sink { desc } => {
1168 let mut dependency_since = Antichain::from_elem(Timestamp::MIN);
1169 for read_hold in dependency_read_holds.iter() {
1170 dependency_since.join_assign(read_hold.since());
1171 }
1172
1173 let [self_hold, read_hold] =
1174 dependency_read_holds.try_into().expect("two holds");
1175
1176 let state = ExportState::new(
1177 desc.instance_id,
1178 read_hold,
1179 self_hold,
1180 write_frontier.clone(),
1181 ReadPolicy::step_back(),
1182 );
1183 maybe_instance_id = Some(state.cluster_id);
1184 extra_state = CollectionStateExtra::Export(state);
1185 }
1186 }
1187
1188 let wallclock_lag_metrics = self.metrics.wallclock_lag_metrics(id, maybe_instance_id);
1189 let collection_state =
1190 CollectionState::new(data_source, metadata, extra_state, wallclock_lag_metrics);
1191
1192 self.collections.insert(id, collection_state);
1193 }
1194
1195 {
1196 let mut source_statistics = self.source_statistics.lock().expect("poisoned");
1197
1198 for id in new_webhook_statistic_entries {
1201 source_statistics.webhook_statistics.entry(id).or_default();
1202 }
1203
1204 }
1208
1209 self.append_shard_mappings(new_collections.into_iter(), Diff::ONE);
1210
1211 for id in to_execute {
1213 match &self.collection(id)?.data_source {
1214 DataSource::Ingestion(ingestion) => {
1215 if !self.read_only
1216 || (ENABLE_0DT_DEPLOYMENT_SOURCES.get(self.config.config_set())
1217 && ingestion.desc.connection.supports_read_only())
1218 {
1219 self.run_ingestion(id)?;
1220 }
1221 }
1222 DataSource::IngestionExport { .. } => unreachable!(
1223 "ingestion exports do not execute directly, but instead schedule their source to be re-executed"
1224 ),
1225 DataSource::Introspection(_)
1226 | DataSource::Webhook
1227 | DataSource::Table
1228 | DataSource::Progress
1229 | DataSource::Other => {}
1230 DataSource::Sink { .. } => {
1231 if !self.read_only {
1232 self.run_export(id)?;
1233 }
1234 }
1235 };
1236 }
1237
1238 Ok(())
1239 }
1240
1241 fn check_alter_ingestion_source_desc(
1242 &mut self,
1243 ingestion_id: GlobalId,
1244 source_desc: &SourceDesc,
1245 ) -> Result<(), StorageError> {
1246 let source_collection = self.collection(ingestion_id)?;
1247 let data_source = &source_collection.data_source;
1248 match &data_source {
1249 DataSource::Ingestion(cur_ingestion) => {
1250 cur_ingestion
1251 .desc
1252 .alter_compatible(ingestion_id, source_desc)?;
1253 }
1254 o => {
1255 tracing::info!(
1256 "{ingestion_id} inalterable because its data source is {:?} and not an ingestion",
1257 o
1258 );
1259 Err(AlterError { id: ingestion_id })?
1260 }
1261 }
1262
1263 Ok(())
1264 }
1265
1266 async fn alter_ingestion_source_desc(
1267 &mut self,
1268 ingestion_ids: BTreeMap<GlobalId, SourceDesc>,
1269 ) -> Result<(), StorageError> {
1270 let mut ingestions_to_run = BTreeSet::new();
1271
1272 for (id, new_desc) in ingestion_ids {
1273 let collection = self
1274 .collections
1275 .get_mut(&id)
1276 .ok_or_else(|| StorageError::IdentifierMissing(id))?;
1277
1278 match &mut collection.data_source {
1279 DataSource::Ingestion(ingestion) => {
1280 if ingestion.desc != new_desc {
1281 tracing::info!(
1282 from = ?ingestion.desc,
1283 to = ?new_desc,
1284 "alter_ingestion_source_desc, updating"
1285 );
1286 ingestion.desc = new_desc;
1287 ingestions_to_run.insert(id);
1288 }
1289 }
1290 o => {
1291 tracing::warn!("alter_ingestion_source_desc called on {:?}", o);
1292 Err(StorageError::IdentifierInvalid(id))?;
1293 }
1294 }
1295 }
1296
1297 for id in ingestions_to_run {
1298 self.run_ingestion(id)?;
1299 }
1300 Ok(())
1301 }
1302
1303 async fn alter_ingestion_connections(
1304 &mut self,
1305 source_connections: BTreeMap<GlobalId, GenericSourceConnection<InlinedConnection>>,
1306 ) -> Result<(), StorageError> {
1307 let mut ingestions_to_run = BTreeSet::new();
1308
1309 for (id, conn) in source_connections {
1310 let collection = self
1311 .collections
1312 .get_mut(&id)
1313 .ok_or_else(|| StorageError::IdentifierMissing(id))?;
1314
1315 match &mut collection.data_source {
1316 DataSource::Ingestion(ingestion) => {
1317 if ingestion.desc.connection != conn {
1320 tracing::info!(from = ?ingestion.desc.connection, to = ?conn, "alter_ingestion_connections, updating");
1321 ingestion.desc.connection = conn;
1322 ingestions_to_run.insert(id);
1323 } else {
1324 tracing::warn!(
1325 "update_source_connection called on {id} but the \
1326 connection was the same"
1327 );
1328 }
1329 }
1330 o => {
1331 tracing::warn!("update_source_connection called on {:?}", o);
1332 Err(StorageError::IdentifierInvalid(id))?;
1333 }
1334 }
1335 }
1336
1337 for id in ingestions_to_run {
1338 self.run_ingestion(id)?;
1339 }
1340 Ok(())
1341 }
1342
1343 async fn alter_ingestion_export_data_configs(
1344 &mut self,
1345 source_exports: BTreeMap<GlobalId, SourceExportDataConfig>,
1346 ) -> Result<(), StorageError> {
1347 let mut ingestions_to_run = BTreeSet::new();
1348
1349 for (source_export_id, new_data_config) in source_exports {
1350 let source_export_collection = self
1353 .collections
1354 .get_mut(&source_export_id)
1355 .ok_or_else(|| StorageError::IdentifierMissing(source_export_id))?;
1356 let ingestion_id = match &mut source_export_collection.data_source {
1357 DataSource::IngestionExport {
1358 ingestion_id,
1359 details: _,
1360 data_config,
1361 } => {
1362 *data_config = new_data_config.clone();
1363 *ingestion_id
1364 }
1365 o => {
1366 tracing::warn!("alter_ingestion_export_data_configs called on {:?}", o);
1367 Err(StorageError::IdentifierInvalid(source_export_id))?
1368 }
1369 };
1370 let ingestion_collection = self
1373 .collections
1374 .get_mut(&ingestion_id)
1375 .ok_or_else(|| StorageError::IdentifierMissing(ingestion_id))?;
1376
1377 match &mut ingestion_collection.data_source {
1378 DataSource::Ingestion(ingestion_desc) => {
1379 let source_export = ingestion_desc
1380 .source_exports
1381 .get_mut(&source_export_id)
1382 .ok_or_else(|| StorageError::IdentifierMissing(source_export_id))?;
1383
1384 if source_export.data_config != new_data_config {
1387 tracing::info!(?source_export_id, from = ?source_export.data_config, to = ?new_data_config, "alter_ingestion_export_data_configs, updating");
1388 source_export.data_config = new_data_config;
1389
1390 ingestions_to_run.insert(ingestion_id);
1391 } else {
1392 tracing::warn!(
1393 "alter_ingestion_export_data_configs called on \
1394 export {source_export_id} of {ingestion_id} but \
1395 the data config was the same"
1396 );
1397 }
1398 }
1399 o => {
1400 tracing::warn!("alter_ingestion_export_data_configs called on {:?}", o);
1401 Err(StorageError::IdentifierInvalid(ingestion_id))?
1402 }
1403 }
1404 }
1405
1406 for id in ingestions_to_run {
1407 self.run_ingestion(id)?;
1408 }
1409 Ok(())
1410 }
1411
1412 async fn alter_table_desc(
1413 &mut self,
1414 existing_collection: GlobalId,
1415 new_collection: GlobalId,
1416 new_desc: RelationDesc,
1417 expected_version: RelationVersion,
1418 ) -> Result<(), StorageError> {
1419 let data_shard = {
1420 let Controller {
1421 collections,
1422 storage_collections,
1423 ..
1424 } = self;
1425
1426 let existing = collections
1427 .get(&existing_collection)
1428 .ok_or(StorageError::IdentifierMissing(existing_collection))?;
1429 if existing.data_source != DataSource::Table {
1430 return Err(StorageError::IdentifierInvalid(existing_collection));
1431 }
1432
1433 storage_collections
1435 .alter_table_desc(
1436 existing_collection,
1437 new_collection,
1438 new_desc.clone(),
1439 expected_version,
1440 )
1441 .await?;
1442
1443 existing.collection_metadata.data_shard.clone()
1444 };
1445
1446 let collection_meta = CollectionMetadata {
1447 persist_location: self.persist_location.clone(),
1448 data_shard,
1449 relation_desc: new_desc.clone(),
1450 txns_shard: Some(self.txns_read.txns_id().clone()),
1452 };
1453 let wallclock_lag_metrics = self.metrics.wallclock_lag_metrics(new_collection, None);
1455 let collection_state = CollectionState::new(
1456 DataSource::Table,
1457 collection_meta,
1458 CollectionStateExtra::None,
1459 wallclock_lag_metrics,
1460 );
1461
1462 self.collections.insert(new_collection, collection_state);
1465
1466 self.append_shard_mappings([new_collection].into_iter(), Diff::ONE);
1467
1468 Ok(())
1469 }
1470
1471 async fn register_table_collections(
1472 &mut self,
1473 register_ts: Timestamp,
1474 ids: Vec<GlobalId>,
1475 ) -> Result<(), StorageError> {
1476 let mut tables = self.table_registrations(ids)?;
1477
1478 if self.read_only {
1480 tables.retain(|table| self.migrated_storage_collections.contains(&table.id));
1481 }
1482 if tables.is_empty() {
1483 return Ok(());
1484 }
1485
1486 match self
1487 .persist_table_worker
1488 .register(register_ts, tables)
1489 .await
1490 {
1491 Ok(res) => res,
1492 Err(_recv) => Err(StorageError::ShuttingDown("persist_table_worker")),
1493 }
1494 }
1495
1496 fn table_registrations(
1497 &self,
1498 ids: Vec<GlobalId>,
1499 ) -> Result<Vec<TableRegistration>, StorageError> {
1500 let mut tables = Vec::with_capacity(ids.len());
1502 for id in ids {
1503 let collection = self.collection(id)?;
1504 if matches!(collection.data_source, DataSource::Table) {
1505 let metadata = &collection.collection_metadata;
1506 tables.push(TableRegistration {
1507 id,
1508 data_shard: metadata.data_shard,
1509 relation_desc: metadata.relation_desc.clone(),
1510 });
1511 }
1512 }
1513 Ok(tables)
1514 }
1515
1516 fn txns_table_ids(&self, ids: Vec<GlobalId>) -> Result<Vec<GlobalId>, StorageError> {
1517 let mut tables = Vec::with_capacity(ids.len());
1518 for id in ids {
1519 let collection = self.collection(id)?;
1520 if matches!(collection.data_source, DataSource::Table) {
1521 tables.push(id);
1522 }
1523 }
1524 Ok(tables)
1525 }
1526
1527 fn export(&self, id: GlobalId) -> Result<&ExportState, StorageError> {
1528 self.collections
1529 .get(&id)
1530 .and_then(|c| match &c.extra_state {
1531 CollectionStateExtra::Export(state) => Some(state),
1532 _ => None,
1533 })
1534 .ok_or(StorageError::IdentifierMissing(id))
1535 }
1536
1537 fn export_mut(&mut self, id: GlobalId) -> Result<&mut ExportState, StorageError> {
1538 self.collections
1539 .get_mut(&id)
1540 .and_then(|c| match &mut c.extra_state {
1541 CollectionStateExtra::Export(state) => Some(state),
1542 _ => None,
1543 })
1544 .ok_or(StorageError::IdentifierMissing(id))
1545 }
1546
1547 async fn create_oneshot_ingestion(
1549 &mut self,
1550 ingestion_id: uuid::Uuid,
1551 collection_id: GlobalId,
1552 instance_id: StorageInstanceId,
1553 request: OneshotIngestionRequest,
1554 result_tx: OneshotResultCallback<ProtoBatch>,
1555 ) -> Result<(), StorageError> {
1556 let collection_meta = self
1557 .collections
1558 .get(&collection_id)
1559 .ok_or_else(|| StorageError::IdentifierMissing(collection_id))?
1560 .collection_metadata
1561 .clone();
1562 let instance = self.instances.get_mut(&instance_id).ok_or_else(|| {
1563 StorageError::Generic(anyhow::anyhow!("missing cluster {instance_id}"))
1565 })?;
1566 let oneshot_cmd = RunOneshotIngestion {
1567 ingestion_id,
1568 collection_id,
1569 collection_meta,
1570 request,
1571 };
1572
1573 if !self.read_only {
1574 instance.send(StorageCommand::RunOneshotIngestion(Box::new(oneshot_cmd)));
1575 let pending = PendingOneshotIngestion {
1576 result_tx,
1577 cluster_id: instance_id,
1578 };
1579 let novel = self
1580 .pending_oneshot_ingestions
1581 .insert(ingestion_id, pending);
1582 assert_none!(novel);
1583 Ok(())
1584 } else {
1585 Err(StorageError::ReadOnly)
1586 }
1587 }
1588
1589 fn cancel_oneshot_ingestion(&mut self, ingestion_id: uuid::Uuid) -> Result<(), StorageError> {
1590 if self.read_only {
1591 return Err(StorageError::ReadOnly);
1592 }
1593
1594 let pending = self
1595 .pending_oneshot_ingestions
1596 .remove(&ingestion_id)
1597 .ok_or_else(|| {
1598 StorageError::Generic(anyhow::anyhow!("missing oneshot ingestion {ingestion_id}"))
1600 })?;
1601
1602 match self.instances.get_mut(&pending.cluster_id) {
1603 Some(instance) => {
1604 instance.send(StorageCommand::CancelOneshotIngestion(ingestion_id));
1605 }
1606 None => {
1607 mz_ore::soft_panic_or_log!(
1608 "canceling oneshot ingestion on non-existent cluster, ingestion {:?}, instance {}",
1609 ingestion_id,
1610 pending.cluster_id,
1611 );
1612 }
1613 }
1614 pending.cancel();
1616
1617 Ok(())
1618 }
1619
1620 async fn alter_export(
1621 &mut self,
1622 id: GlobalId,
1623 new_description: ExportDescription,
1624 ) -> Result<(), StorageError> {
1625 let from_id = new_description.sink.from;
1626
1627 let desired_read_holds = vec![from_id.clone(), id.clone()];
1630 let [input_hold, self_hold] = self
1631 .storage_collections
1632 .acquire_read_holds(desired_read_holds)
1633 .expect("missing dependency")
1634 .try_into()
1635 .expect("expected number of holds");
1636 let from_storage_metadata = self.storage_collections.collection_metadata(from_id)?;
1637 let to_storage_metadata = self.storage_collections.collection_metadata(id)?;
1638
1639 let cur_export = self.export_mut(id)?;
1641 let input_readable = cur_export
1642 .write_frontier
1643 .iter()
1644 .all(|t| input_hold.since().less_than(t));
1645 if !input_readable {
1646 return Err(StorageError::ReadBeforeSince(from_id));
1647 }
1648
1649 let new_export = ExportState {
1650 read_capabilities: cur_export.read_capabilities.clone(),
1651 cluster_id: new_description.instance_id,
1652 derived_since: cur_export.derived_since.clone(),
1653 read_holds: [input_hold, self_hold],
1654 read_policy: cur_export.read_policy.clone(),
1655 write_frontier: cur_export.write_frontier.clone(),
1656 };
1657 *cur_export = new_export;
1658
1659 let with_snapshot = new_description.sink.with_snapshot
1666 && !PartialOrder::less_than(&new_description.sink.as_of, &cur_export.write_frontier);
1667
1668 let cmd = RunSinkCommand {
1669 id,
1670 description: StorageSinkDesc {
1671 from: from_id,
1672 from_desc: new_description.sink.from_desc,
1673 connection: new_description.sink.connection,
1674 envelope: new_description.sink.envelope,
1675 as_of: new_description.sink.as_of,
1676 version: new_description.sink.version,
1677 from_storage_metadata,
1678 with_snapshot,
1679 to_storage_metadata,
1680 commit_interval: new_description.sink.commit_interval,
1681 },
1682 };
1683
1684 let instance = self
1686 .instances
1687 .get_mut(&new_description.instance_id)
1688 .ok_or_else(|| StorageError::ExportInstanceMissing {
1689 storage_instance_id: new_description.instance_id,
1690 export_id: id,
1691 })?;
1692
1693 instance.send(StorageCommand::RunSink(Box::new(cmd)));
1694 Ok(())
1695 }
1696
1697 async fn alter_export_connections(
1699 &mut self,
1700 exports: BTreeMap<GlobalId, StorageSinkConnection>,
1701 ) -> Result<(), StorageError> {
1702 let mut updates_by_instance =
1703 BTreeMap::<StorageInstanceId, Vec<(RunSinkCommand, ExportDescription)>>::new();
1704
1705 for (id, connection) in exports {
1706 let (mut new_export_description, as_of): (ExportDescription, _) = {
1714 let export = &self.collections[&id];
1715 let DataSource::Sink { desc } = &export.data_source else {
1716 panic!("export exists")
1717 };
1718 let CollectionStateExtra::Export(state) = &export.extra_state else {
1719 panic!("export exists")
1720 };
1721 let export_description = desc.clone();
1722 let as_of = state.input_hold().since().clone();
1723
1724 (export_description, as_of)
1725 };
1726 let current_sink = new_export_description.sink.clone();
1727
1728 new_export_description.sink.connection = connection;
1729
1730 current_sink.alter_compatible(id, &new_export_description.sink)?;
1732
1733 let from_storage_metadata = self
1734 .storage_collections
1735 .collection_metadata(new_export_description.sink.from)?;
1736 let to_storage_metadata = self.storage_collections.collection_metadata(id)?;
1737
1738 let cmd = RunSinkCommand {
1739 id,
1740 description: StorageSinkDesc {
1741 from: new_export_description.sink.from,
1742 from_desc: new_export_description.sink.from_desc.clone(),
1743 connection: new_export_description.sink.connection.clone(),
1744 envelope: new_export_description.sink.envelope,
1745 with_snapshot: new_export_description.sink.with_snapshot,
1746 version: new_export_description.sink.version,
1747 as_of: as_of.to_owned(),
1758 from_storage_metadata,
1759 to_storage_metadata,
1760 commit_interval: new_export_description.sink.commit_interval,
1761 },
1762 };
1763
1764 let update = updates_by_instance
1765 .entry(new_export_description.instance_id)
1766 .or_default();
1767 update.push((cmd, new_export_description));
1768 }
1769
1770 for (instance_id, updates) in updates_by_instance {
1771 let mut export_updates = BTreeMap::new();
1772 let mut cmds = Vec::with_capacity(updates.len());
1773
1774 for (cmd, export_state) in updates {
1775 export_updates.insert(cmd.id, export_state);
1776 cmds.push(cmd);
1777 }
1778
1779 let instance = self.instances.get_mut(&instance_id).ok_or_else(|| {
1781 StorageError::ExportInstanceMissing {
1782 storage_instance_id: instance_id,
1783 export_id: *export_updates
1784 .keys()
1785 .next()
1786 .expect("set of exports not empty"),
1787 }
1788 })?;
1789
1790 for cmd in cmds {
1791 instance.send(StorageCommand::RunSink(Box::new(cmd)));
1792 }
1793
1794 for (id, new_export_description) in export_updates {
1796 let Some(state) = self.collections.get_mut(&id) else {
1797 panic!("export known to exist")
1798 };
1799 let DataSource::Sink { desc } = &mut state.data_source else {
1800 panic!("export known to exist")
1801 };
1802 *desc = new_export_description;
1803 }
1804 }
1805
1806 Ok(())
1807 }
1808
1809 fn drop_tables(
1810 &mut self,
1811 storage_metadata: &StorageMetadata,
1812 identifiers: Vec<GlobalId>,
1813 ) -> Result<(), StorageError> {
1814 let (table_write_ids, data_source_ids): (Vec<_>, Vec<_>) = identifiers
1815 .into_iter()
1816 .partition(|id| match self.collections[id].data_source {
1817 DataSource::Table => true,
1818 DataSource::IngestionExport { .. } | DataSource::Webhook => false,
1819 _ => panic!("identifier is not a table: {}", id),
1820 });
1821
1822 if table_write_ids.len() > 0 {
1823 let tx = self.pending_table_handle_drops_tx.clone();
1824 for identifier in table_write_ids {
1825 let _ = tx.send(identifier);
1826 }
1827 }
1828
1829 if data_source_ids.len() > 0 {
1830 self.validate_collection_ids(data_source_ids.iter().cloned())?;
1831 self.drop_sources_unvalidated(storage_metadata, data_source_ids)?;
1832 }
1833
1834 Ok(())
1835 }
1836
1837 fn drop_sources(
1838 &mut self,
1839 storage_metadata: &StorageMetadata,
1840 identifiers: Vec<GlobalId>,
1841 ) -> Result<(), StorageError> {
1842 self.validate_collection_ids(identifiers.iter().cloned())?;
1843 self.drop_sources_unvalidated(storage_metadata, identifiers)
1844 }
1845
1846 fn drop_sources_unvalidated(
1847 &mut self,
1848 storage_metadata: &StorageMetadata,
1849 ids: Vec<GlobalId>,
1850 ) -> Result<(), StorageError> {
1851 let mut ingestions_to_execute = BTreeSet::new();
1854 let mut ingestions_to_drop = BTreeSet::new();
1855 let mut source_statistics_to_drop = Vec::new();
1856
1857 let mut collections_to_drop = Vec::new();
1861
1862 for id in ids.iter() {
1863 let collection_state = self.collections.get(id);
1864
1865 if let Some(collection_state) = collection_state {
1866 match collection_state.data_source {
1867 DataSource::Webhook => {
1868 let fut = self.collection_manager.unregister_collection(*id);
1871 mz_ore::task::spawn(|| format!("storage-webhook-cleanup-{id}"), fut);
1872
1873 collections_to_drop.push(*id);
1874 source_statistics_to_drop.push(*id);
1875 }
1876 DataSource::Ingestion(_) => {
1877 ingestions_to_drop.insert(*id);
1878 source_statistics_to_drop.push(*id);
1879 }
1880 DataSource::IngestionExport { ingestion_id, .. } => {
1881 ingestions_to_execute.insert(ingestion_id);
1888
1889 let ingestion_state = match self.collections.get_mut(&ingestion_id) {
1891 Some(ingestion_collection) => ingestion_collection,
1892 None => {
1894 tracing::error!(
1895 "primary source {ingestion_id} seemingly dropped before subsource {id}"
1896 );
1897 continue;
1898 }
1899 };
1900
1901 match &mut ingestion_state.data_source {
1902 DataSource::Ingestion(ingestion_desc) => {
1903 let removed = ingestion_desc.source_exports.remove(id);
1904 mz_ore::soft_assert_or_log!(
1905 removed.is_some(),
1906 "dropped subsource {id} already removed from source exports"
1907 );
1908 }
1909 _ => unreachable!(
1910 "SourceExport must only refer to primary sources that already exist"
1911 ),
1912 };
1913
1914 ingestions_to_drop.insert(*id);
1918 source_statistics_to_drop.push(*id);
1919 }
1920 DataSource::Progress | DataSource::Table | DataSource::Other => {
1921 collections_to_drop.push(*id);
1922 }
1923 DataSource::Introspection(_) | DataSource::Sink { .. } => {
1924 soft_panic_or_log!(
1927 "drop_sources called on a {:?} (id={id}))",
1928 collection_state.data_source,
1929 );
1930 }
1931 }
1932 }
1933 }
1934
1935 ingestions_to_execute.retain(|id| !ingestions_to_drop.contains(id));
1937 for ingestion_id in ingestions_to_execute {
1938 self.run_ingestion(ingestion_id)?;
1939 }
1940
1941 let ingestion_policies = ingestions_to_drop
1948 .iter()
1949 .map(|id| (*id, ReadPolicy::ValidFrom(Antichain::new())))
1950 .collect();
1951
1952 tracing::debug!(
1953 ?ingestion_policies,
1954 "dropping sources by setting read hold policies"
1955 );
1956 self.set_hold_policies(ingestion_policies);
1957
1958 let shards_to_update: BTreeSet<_> = ingestions_to_drop
1960 .iter()
1961 .chain(collections_to_drop.iter())
1962 .cloned()
1963 .collect();
1964 self.append_shard_mappings(shards_to_update.into_iter(), Diff::MINUS_ONE);
1965
1966 let status_now = mz_ore::now::to_datetime((self.now)());
1967 let mut status_updates = vec![];
1968 for id in ingestions_to_drop.iter() {
1969 status_updates.push(StatusUpdate::new(*id, status_now, Status::Dropped));
1970 }
1971
1972 if !self.read_only {
1973 self.append_status_introspection_updates(
1974 IntrospectionType::SourceStatusHistory,
1975 status_updates,
1976 );
1977 }
1978
1979 {
1980 let mut source_statistics = self.source_statistics.lock().expect("poisoned");
1981 for id in source_statistics_to_drop {
1982 source_statistics
1983 .source_statistics
1984 .retain(|(stats_id, _), _| stats_id != &id);
1985 source_statistics
1986 .webhook_statistics
1987 .retain(|stats_id, _| stats_id != &id);
1988 }
1989 }
1990
1991 for id in ingestions_to_drop.iter().chain(collections_to_drop.iter()) {
1993 tracing::info!(%id, "dropping collection state");
1994 let collection = self
1995 .collections
1996 .remove(id)
1997 .expect("list populated after checking that self.collections contains it");
1998
1999 let instance = match &collection.extra_state {
2000 CollectionStateExtra::Ingestion(ingestion) => Some(ingestion.instance_id),
2001 CollectionStateExtra::Export(export) => Some(export.cluster_id()),
2002 CollectionStateExtra::None => None,
2003 }
2004 .and_then(|i| self.instances.get(&i));
2005
2006 if let Some(instance) = instance {
2010 let active_replicas = instance.get_active_replicas_for_object(id);
2011 if !active_replicas.is_empty() {
2012 match &collection.data_source {
2019 DataSource::Ingestion(ingestion_desc) => {
2020 if *id != ingestion_desc.remap_collection_id {
2021 self.dropped_objects.insert(
2022 ingestion_desc.remap_collection_id,
2023 active_replicas.clone(),
2024 );
2025 }
2026 }
2027 _ => {}
2028 }
2029
2030 self.dropped_objects.insert(*id, active_replicas);
2031 }
2032 }
2033 }
2034
2035 self.storage_collections
2037 .drop_collections_unvalidated(storage_metadata, ids);
2038
2039 Ok(())
2040 }
2041
2042 fn drop_sinks(
2044 &mut self,
2045 storage_metadata: &StorageMetadata,
2046 identifiers: Vec<GlobalId>,
2047 ) -> Result<(), StorageError> {
2048 self.validate_export_ids(identifiers.iter().cloned())?;
2049 self.drop_sinks_unvalidated(storage_metadata, identifiers);
2050 Ok(())
2051 }
2052
2053 fn drop_sinks_unvalidated(
2054 &mut self,
2055 storage_metadata: &StorageMetadata,
2056 mut sinks_to_drop: Vec<GlobalId>,
2057 ) {
2058 sinks_to_drop.retain(|id| self.export(*id).is_ok());
2060
2061 let drop_policy = sinks_to_drop
2068 .iter()
2069 .map(|id| (*id, ReadPolicy::ValidFrom(Antichain::new())))
2070 .collect();
2071
2072 tracing::debug!(
2073 ?drop_policy,
2074 "dropping sources by setting read hold policies"
2075 );
2076 self.set_hold_policies(drop_policy);
2077
2078 let status_now = mz_ore::now::to_datetime((self.now)());
2085
2086 let mut status_updates = vec![];
2088 {
2089 let mut sink_statistics = self.sink_statistics.lock().expect("poisoned");
2090 for id in sinks_to_drop.iter() {
2091 status_updates.push(StatusUpdate::new(*id, status_now, Status::Dropped));
2092 sink_statistics.retain(|(stats_id, _), _| stats_id != id);
2093 }
2094 }
2095
2096 if !self.read_only {
2097 self.append_status_introspection_updates(
2098 IntrospectionType::SinkStatusHistory,
2099 status_updates,
2100 );
2101 }
2102
2103 for id in sinks_to_drop.iter() {
2105 tracing::info!(%id, "dropping export state");
2106 let collection = self
2107 .collections
2108 .remove(id)
2109 .expect("list populated after checking that self.collections contains it");
2110
2111 let instance = match &collection.extra_state {
2112 CollectionStateExtra::Ingestion(ingestion) => Some(ingestion.instance_id),
2113 CollectionStateExtra::Export(export) => Some(export.cluster_id()),
2114 CollectionStateExtra::None => None,
2115 }
2116 .and_then(|i| self.instances.get(&i));
2117
2118 if let Some(instance) = instance {
2122 let active_replicas = instance.get_active_replicas_for_object(id);
2123 if !active_replicas.is_empty() {
2124 self.dropped_objects.insert(*id, active_replicas);
2125 }
2126 }
2127 }
2128
2129 self.storage_collections
2131 .drop_collections_unvalidated(storage_metadata, sinks_to_drop);
2132 }
2133
2134 #[instrument(level = "debug")]
2135 fn append_table(
2136 &mut self,
2137 write_ts: Timestamp,
2138 advance_to: Timestamp,
2139 commands: Vec<(GlobalId, Vec<TableData>)>,
2140 ) -> Result<tokio::sync::oneshot::Receiver<Result<(), StorageError>>, StorageError> {
2141 if self.read_only {
2142 if !commands
2145 .iter()
2146 .all(|(id, _)| id.is_system() && self.migrated_storage_collections.contains(id))
2147 {
2148 return Err(StorageError::ReadOnly);
2149 }
2150 }
2151
2152 for (id, updates) in commands.iter() {
2154 if !updates.is_empty() {
2155 if !write_ts.less_than(&advance_to) {
2156 return Err(StorageError::UpdateBeyondUpper(*id));
2157 }
2158 }
2159 }
2160
2161 Ok(self
2162 .persist_table_worker
2163 .append(write_ts, advance_to, commands))
2164 }
2165
2166 fn table_write_handle(&self) -> Arc<dyn mz_storage_client::controller::TableWriteHandle> {
2167 Arc::new(persist_handles::TableWriteWorkerHandle(
2168 self.persist_table_worker.clone(),
2169 ))
2170 }
2171
2172 fn monotonic_appender(&self, id: GlobalId) -> Result<MonotonicAppender, StorageError> {
2173 self.collection_manager.monotonic_appender(id)
2174 }
2175
2176 fn webhook_statistics(&self, id: GlobalId) -> Result<Arc<WebhookStatistics>, StorageError> {
2177 let source_statistics = self.source_statistics.lock().expect("poisoned");
2179 source_statistics
2180 .webhook_statistics
2181 .get(&id)
2182 .cloned()
2183 .ok_or(StorageError::IdentifierMissing(id))
2184 }
2185
2186 async fn ready(&mut self) {
2187 if self.maintenance_scheduled {
2188 return;
2189 }
2190
2191 if !self.pending_table_handle_drops_rx.is_empty() {
2192 return;
2193 }
2194
2195 tokio::select! {
2196 Some(m) = self.instance_response_rx.recv() => {
2197 self.stashed_responses.push(m);
2198 while let Ok(m) = self.instance_response_rx.try_recv() {
2199 self.stashed_responses.push(m);
2200 }
2201 }
2202 _ = self.maintenance_ticker.tick() => {
2203 self.maintenance_scheduled = true;
2204 },
2205 };
2206 }
2207
2208 #[instrument(level = "debug")]
2209 fn process(
2210 &mut self,
2211 storage_metadata: &StorageMetadata,
2212 ) -> Result<Option<Response>, anyhow::Error> {
2213 if self.maintenance_scheduled {
2215 self.maintain();
2216 self.maintenance_scheduled = false;
2217 }
2218
2219 for instance in self.instances.values_mut() {
2220 instance.rehydrate_failed_replicas();
2221 }
2222
2223 let mut status_updates = vec![];
2224 let mut updated_frontiers = BTreeMap::new();
2225
2226 let stashed_responses = std::mem::take(&mut self.stashed_responses);
2228 for resp in stashed_responses {
2229 match resp {
2230 (_replica_id, StorageResponse::FrontierUpper(id, upper)) => {
2231 self.update_write_frontier(id, &upper);
2232 updated_frontiers.insert(id, upper);
2233 }
2234 (replica_id, StorageResponse::DroppedId(id)) => {
2235 let replica_id = replica_id.expect("DroppedId from unknown replica");
2236 if let Some(remaining_replicas) = self.dropped_objects.get_mut(&id) {
2237 remaining_replicas.remove(&replica_id);
2238 if remaining_replicas.is_empty() {
2239 self.dropped_objects.remove(&id);
2240 }
2241 } else {
2242 soft_panic_or_log!("unexpected DroppedId for {id}");
2243 }
2244 }
2245 (replica_id, StorageResponse::StatisticsUpdates(source_stats, sink_stats)) => {
2246 {
2248 let replica_id = if let Some(replica_id) = replica_id {
2255 replica_id
2256 } else {
2257 tracing::error!(
2258 ?source_stats,
2259 "missing replica_id for source statistics update"
2260 );
2261 continue;
2262 };
2263
2264 let mut shared_stats = self.source_statistics.lock().expect("poisoned");
2265
2266 for stat in source_stats {
2267 let collection_id = stat.id.clone();
2268
2269 if self.collection(collection_id).is_err() {
2270 continue;
2273 }
2274
2275 let entry = shared_stats
2276 .source_statistics
2277 .entry((stat.id, Some(replica_id)));
2278
2279 match entry {
2280 btree_map::Entry::Vacant(vacant_entry) => {
2281 let mut stats = ControllerSourceStatistics::new(
2282 collection_id,
2283 Some(replica_id),
2284 );
2285 stats.incorporate(stat);
2286 vacant_entry.insert(stats);
2287 }
2288 btree_map::Entry::Occupied(mut occupied_entry) => {
2289 occupied_entry.get_mut().incorporate(stat);
2290 }
2291 }
2292 }
2293 }
2294
2295 {
2296 let replica_id = if let Some(replica_id) = replica_id {
2307 replica_id
2308 } else {
2309 tracing::error!(
2310 ?sink_stats,
2311 "missing replica_id for sink statistics update"
2312 );
2313 continue;
2314 };
2315
2316 let mut shared_stats = self.sink_statistics.lock().expect("poisoned");
2317
2318 for stat in sink_stats {
2319 let collection_id = stat.id.clone();
2320
2321 if self.collection(collection_id).is_err() {
2322 continue;
2325 }
2326
2327 let entry = shared_stats.entry((stat.id, Some(replica_id)));
2328
2329 match entry {
2330 btree_map::Entry::Vacant(vacant_entry) => {
2331 let mut stats =
2332 ControllerSinkStatistics::new(collection_id, replica_id);
2333 stats.incorporate(stat);
2334 vacant_entry.insert(stats);
2335 }
2336 btree_map::Entry::Occupied(mut occupied_entry) => {
2337 occupied_entry.get_mut().incorporate(stat);
2338 }
2339 }
2340 }
2341 }
2342 }
2343 (replica_id, StorageResponse::StatusUpdate(mut status_update)) => {
2344 match status_update.status {
2360 Status::Running => {
2361 let collection = self.collections.get_mut(&status_update.id);
2362 match collection {
2363 Some(collection) => {
2364 match collection.extra_state {
2365 CollectionStateExtra::Ingestion(
2366 ref mut ingestion_state,
2367 ) => {
2368 if ingestion_state.hydrated_on.is_empty() {
2369 tracing::debug!(ingestion_id = %status_update.id, "ingestion is hydrated");
2370 }
2371 ingestion_state.hydrated_on.insert(replica_id.expect(
2372 "replica id should be present for status running",
2373 ));
2374 }
2375 CollectionStateExtra::Export(_) => {
2376 }
2378 CollectionStateExtra::None => {
2379 }
2381 }
2382 }
2383 None => (), }
2386 }
2387 Status::Paused => {
2388 let collection = self.collections.get_mut(&status_update.id);
2389 match collection {
2390 Some(collection) => {
2391 match collection.extra_state {
2392 CollectionStateExtra::Ingestion(
2393 ref mut ingestion_state,
2394 ) => {
2395 tracing::debug!(ingestion_id = %status_update.id, "ingestion is now paused");
2402 ingestion_state.hydrated_on.clear();
2403 }
2404 CollectionStateExtra::Export(_) => {
2405 }
2407 CollectionStateExtra::None => {
2408 }
2410 }
2411 }
2412 None => (), }
2415 }
2416 _ => (),
2417 }
2418
2419 if let Some(id) = replica_id {
2421 status_update.replica_id = Some(id);
2422 }
2423 status_updates.push(status_update);
2424 }
2425 (_replica_id, StorageResponse::StagedBatches(batches)) => {
2426 for (ingestion_id, batches) in batches {
2427 match self.pending_oneshot_ingestions.remove(&ingestion_id) {
2428 Some(pending) => {
2429 if let Some(instance) = self.instances.get_mut(&pending.cluster_id)
2432 {
2433 instance
2434 .send(StorageCommand::CancelOneshotIngestion(ingestion_id));
2435 }
2436 (pending.result_tx)(batches)
2438 }
2439 None => {
2440 }
2443 }
2444 }
2445 }
2446 }
2447 }
2448
2449 self.record_status_updates(status_updates);
2450
2451 let mut dropped_table_ids = Vec::new();
2453 while let Ok(dropped_id) = self.pending_table_handle_drops_rx.try_recv() {
2454 dropped_table_ids.push(dropped_id);
2455 }
2456 if !dropped_table_ids.is_empty() {
2457 self.drop_sources(storage_metadata, dropped_table_ids)?;
2458 }
2459
2460 if updated_frontiers.is_empty() {
2461 Ok(None)
2462 } else {
2463 Ok(Some(Response::FrontierUpdates(
2464 updated_frontiers.into_iter().collect(),
2465 )))
2466 }
2467 }
2468
2469 async fn inspect_persist_state(
2470 &self,
2471 id: GlobalId,
2472 ) -> Result<serde_json::Value, anyhow::Error> {
2473 let collection = &self.storage_collections.collection_metadata(id)?;
2474 let client = self
2475 .persist
2476 .open(collection.persist_location.clone())
2477 .await?;
2478 let shard_state = client
2479 .inspect_shard::<Timestamp>(&collection.data_shard)
2480 .await?;
2481 let json_state = serde_json::to_value(shard_state)?;
2482 Ok(json_state)
2483 }
2484
2485 fn append_introspection_updates(
2486 &mut self,
2487 type_: IntrospectionType,
2488 updates: Vec<(Row, Diff)>,
2489 ) {
2490 let id = self.introspection_ids[&type_];
2491 let updates = updates.into_iter().map(|update| update.into()).collect();
2492 self.collection_manager.blind_write(id, updates);
2493 }
2494
2495 fn append_status_introspection_updates(
2496 &mut self,
2497 type_: IntrospectionType,
2498 updates: Vec<StatusUpdate>,
2499 ) {
2500 let id = self.introspection_ids[&type_];
2501 let updates: Vec<_> = updates.into_iter().map(|update| update.into()).collect();
2502 if !updates.is_empty() {
2503 self.collection_manager.blind_write(id, updates);
2504 }
2505 }
2506
2507 fn update_introspection_collection(&mut self, type_: IntrospectionType, op: StorageWriteOp) {
2508 let id = self.introspection_ids[&type_];
2509 self.collection_manager.differential_write(id, op);
2510 }
2511
2512 fn append_only_introspection_tx(
2513 &self,
2514 type_: IntrospectionType,
2515 ) -> mpsc::UnboundedSender<(
2516 Vec<AppendOnlyUpdate>,
2517 oneshot::Sender<Result<(), StorageError>>,
2518 )> {
2519 let id = self.introspection_ids[&type_];
2520 self.collection_manager.append_only_write_sender(id)
2521 }
2522
2523 fn differential_introspection_tx(
2524 &self,
2525 type_: IntrospectionType,
2526 ) -> mpsc::UnboundedSender<(StorageWriteOp, oneshot::Sender<Result<(), StorageError>>)> {
2527 let id = self.introspection_ids[&type_];
2528 self.collection_manager.differential_write_sender(id)
2529 }
2530
2531 async fn real_time_recent_timestamp(
2532 &self,
2533 timestamp_objects: BTreeSet<GlobalId>,
2534 timeout: Duration,
2535 ) -> Result<BoxFuture<Result<Timestamp, StorageError>>, StorageError> {
2536 use mz_storage_types::sources::GenericSourceConnection;
2537
2538 let mut rtr_futures = BTreeMap::new();
2539
2540 for id in timestamp_objects.into_iter().filter(GlobalId::is_user) {
2542 let collection = match self.collection(id) {
2543 Ok(c) => c,
2544 Err(_) => continue,
2546 };
2547
2548 let (source_conn, remap_id) = match &collection.data_source {
2549 DataSource::Ingestion(IngestionDescription {
2550 desc: SourceDesc { connection, .. },
2551 remap_collection_id,
2552 ..
2553 }) => match connection {
2554 GenericSourceConnection::Kafka(_)
2555 | GenericSourceConnection::Postgres(_)
2556 | GenericSourceConnection::MySql(_)
2557 | GenericSourceConnection::SqlServer(_) => {
2558 (connection.clone(), *remap_collection_id)
2559 }
2560
2561 GenericSourceConnection::LoadGenerator(_) => continue,
2566 },
2567 _ => {
2569 continue;
2570 }
2571 };
2572
2573 let config = self.config().clone();
2575
2576 let read_handle = self.read_handle_for_snapshot(remap_id).await?;
2584
2585 let remap_read_hold = self
2588 .storage_collections
2589 .acquire_read_holds(vec![remap_id])
2590 .map_err(|_e| StorageError::ReadBeforeSince(remap_id))?
2591 .expect_element(|| "known to be exactly one");
2592
2593 let remap_as_of = remap_read_hold
2594 .since()
2595 .to_owned()
2596 .into_option()
2597 .ok_or(StorageError::ReadBeforeSince(remap_id))?;
2598
2599 rtr_futures.insert(
2600 id,
2601 tokio::time::timeout(timeout, async move {
2602 use mz_storage_types::sources::SourceConnection as _;
2603
2604 let as_of = Antichain::from_elem(remap_as_of);
2607 let remap_subscribe = read_handle
2608 .subscribe(as_of.clone())
2609 .await
2610 .map_err(|_| StorageError::ReadBeforeSince(remap_id))?;
2611
2612 tracing::debug!(?id, type_ = source_conn.name(), upstream = ?source_conn.external_reference(), "fetching real time recency");
2613
2614 let result = rtr::real_time_recency_ts(
2615 source_conn,
2616 id,
2617 config,
2618 as_of,
2619 remap_subscribe,
2620 )
2621 .await
2622 .map_err(|e| {
2623 tracing::debug!(?id, "real time recency error: {:?}", e);
2624 e
2625 });
2626
2627 drop(remap_read_hold);
2629
2630 result
2631 }),
2632 );
2633 }
2634
2635 Ok(Box::pin(async move {
2636 let (ids, futs): (Vec<_>, Vec<_>) = rtr_futures.into_iter().unzip();
2637 ids.into_iter()
2638 .zip_eq(futures::future::join_all(futs).await)
2639 .try_fold(Timestamp::MIN, |curr, (id, per_source_res)| {
2640 let new =
2641 per_source_res.map_err(|_e: Elapsed| StorageError::RtrTimeout(id))??;
2642 Ok::<_, StorageError>(std::cmp::max(curr, new))
2643 })
2644 }))
2645 }
2646
2647 fn dump(&self) -> Result<serde_json::Value, anyhow::Error> {
2648 let Self {
2650 build_info: _,
2651 now: _,
2652 read_only,
2653 collections,
2654 dropped_objects,
2655 persist_table_worker: _,
2656 txns_read: _,
2657 txns_metrics: _,
2658 stashed_responses,
2659 pending_table_handle_drops_tx: _,
2660 pending_table_handle_drops_rx: _,
2661 pending_oneshot_ingestions,
2662 collection_manager: _,
2663 introspection_ids,
2664 introspection_tokens: _,
2665 source_statistics: _,
2666 sink_statistics: _,
2667 statistics_interval_sender: _,
2668 instances,
2669 initialized,
2670 config,
2671 persist_location,
2672 persist: _,
2673 metrics: _,
2674 recorded_frontiers,
2675 recorded_replica_frontiers,
2676 wallclock_lag: _,
2677 wallclock_lag_last_recorded,
2678 storage_collections: _,
2679 migrated_storage_collections,
2680 maintenance_ticker: _,
2681 maintenance_scheduled,
2682 instance_response_tx: _,
2683 instance_response_rx: _,
2684 persist_warm_task: _,
2685 } = self;
2686
2687 let collections: BTreeMap<_, _> = collections
2688 .iter()
2689 .map(|(id, c)| (id.to_string(), format!("{c:?}")))
2690 .collect();
2691 let dropped_objects: BTreeMap<_, _> = dropped_objects
2692 .iter()
2693 .map(|(id, rs)| (id.to_string(), format!("{rs:?}")))
2694 .collect();
2695 let stashed_responses: Vec<_> =
2696 stashed_responses.iter().map(|r| format!("{r:?}")).collect();
2697 let pending_oneshot_ingestions: BTreeMap<_, _> = pending_oneshot_ingestions
2698 .iter()
2699 .map(|(uuid, i)| (uuid.to_string(), format!("{i:?}")))
2700 .collect();
2701 let introspection_ids: BTreeMap<_, _> = introspection_ids
2702 .iter()
2703 .map(|(typ, id)| (format!("{typ:?}"), id.to_string()))
2704 .collect();
2705 let instances: BTreeMap<_, _> = instances
2706 .iter()
2707 .map(|(id, i)| (id.to_string(), format!("{i:?}")))
2708 .collect();
2709 let recorded_frontiers: BTreeMap<_, _> = recorded_frontiers
2710 .iter()
2711 .map(|(id, fs)| (id.to_string(), format!("{fs:?}")))
2712 .collect();
2713 let recorded_replica_frontiers: Vec<_> = recorded_replica_frontiers
2714 .iter()
2715 .map(|((gid, rid), f)| (gid.to_string(), rid.to_string(), format!("{f:?}")))
2716 .collect();
2717 let migrated_storage_collections: Vec<_> = migrated_storage_collections
2718 .iter()
2719 .map(|id| id.to_string())
2720 .collect();
2721
2722 Ok(serde_json::json!({
2723 "read_only": read_only,
2724 "collections": collections,
2725 "dropped_objects": dropped_objects,
2726 "stashed_responses": stashed_responses,
2727 "pending_oneshot_ingestions": pending_oneshot_ingestions,
2728 "introspection_ids": introspection_ids,
2729 "instances": instances,
2730 "initialized": initialized,
2731 "config": format!("{config:?}"),
2732 "persist_location": format!("{persist_location:?}"),
2733 "recorded_frontiers": recorded_frontiers,
2734 "recorded_replica_frontiers": recorded_replica_frontiers,
2735 "wallclock_lag_last_recorded": format!("{wallclock_lag_last_recorded:?}"),
2736 "migrated_storage_collections": migrated_storage_collections,
2737 "maintenance_scheduled": maintenance_scheduled,
2738 }))
2739 }
2740}
2741
2742pub fn prepare_initialization(txn: &mut dyn StorageTxn) -> Result<(), StorageError> {
2749 if txn.get_txn_wal_shard().is_none() {
2750 let txns_id = ShardId::new();
2751 txn.write_txn_wal_shard(txns_id)?;
2752 }
2753
2754 Ok(())
2755}
2756
2757impl Controller
2758where
2759 Self: StorageController,
2760{
2761 pub async fn new(
2769 build_info: &'static BuildInfo,
2770 persist_location: PersistLocation,
2771 persist_clients: Arc<PersistClientCache>,
2772 now: NowFn,
2773 wallclock_lag: WallclockLagFn<Timestamp>,
2774 txns_metrics: Arc<TxnMetrics>,
2775 read_only: bool,
2776 metrics_registry: &MetricsRegistry,
2777 controller_metrics: ControllerMetrics,
2778 connection_context: ConnectionContext,
2779 txn: &dyn StorageTxn,
2780 storage_collections: Arc<dyn StorageCollections + Send + Sync>,
2781 ) -> Self {
2782 let txns_client = persist_clients
2783 .open(persist_location.clone())
2784 .await
2785 .expect("location should be valid");
2786
2787 let persist_warm_task = warm_persist_state_in_background(
2788 txns_client.clone(),
2789 txn.get_collection_metadata().into_values(),
2790 );
2791 let persist_warm_task = Some(persist_warm_task.abort_on_drop());
2792
2793 let txns_id = txn
2797 .get_txn_wal_shard()
2798 .expect("must call prepare initialization before creating storage controller");
2799
2800 let persist_table_worker = if read_only {
2801 let txns_write = txns_client
2802 .open_writer(
2803 txns_id,
2804 Arc::new(TxnsCodecRow::desc()),
2805 Arc::new(UnitSchema),
2806 Diagnostics {
2807 shard_name: "txns".to_owned(),
2808 handle_purpose: "follow txns upper".to_owned(),
2809 },
2810 )
2811 .await
2812 .expect("txns schema shouldn't change");
2813 persist_handles::PersistTableWriteWorker::new_read_only_mode(
2814 txns_write,
2815 txns_client.clone(),
2816 )
2817 } else {
2818 let mut txns = TxnsHandle::open(
2819 Timestamp::MIN,
2820 txns_client.clone(),
2821 txns_client.dyncfgs().clone(),
2822 Arc::clone(&txns_metrics),
2823 txns_id,
2824 Opaque::encode(&PersistEpoch::default()),
2825 )
2826 .await;
2827 txns.upgrade_version().await;
2828 persist_handles::PersistTableWriteWorker::new_txns(txns, txns_client.clone())
2829 };
2830 let txns_read = TxnsRead::start::<TxnsCodecRow>(txns_client.clone(), txns_id).await;
2831
2832 let collection_manager = collection_mgmt::CollectionManager::new(read_only, now.clone());
2833
2834 let introspection_ids = BTreeMap::new();
2835 let introspection_tokens = Arc::new(Mutex::new(BTreeMap::new()));
2836
2837 let (statistics_interval_sender, _) =
2838 channel(mz_storage_types::parameters::STATISTICS_INTERVAL_DEFAULT);
2839
2840 let (pending_table_handle_drops_tx, pending_table_handle_drops_rx) =
2841 tokio::sync::mpsc::unbounded_channel();
2842
2843 let mut maintenance_ticker = tokio::time::interval(Duration::from_secs(1));
2844 maintenance_ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
2845
2846 let (instance_response_tx, instance_response_rx) = mpsc::unbounded_channel();
2847
2848 let metrics = StorageControllerMetrics::new(metrics_registry, controller_metrics);
2849
2850 let now_dt = mz_ore::now::to_datetime(now());
2851
2852 Self {
2853 build_info,
2854 collections: BTreeMap::default(),
2855 dropped_objects: Default::default(),
2856 persist_table_worker,
2857 txns_read,
2858 txns_metrics,
2859 stashed_responses: vec![],
2860 pending_table_handle_drops_tx,
2861 pending_table_handle_drops_rx,
2862 pending_oneshot_ingestions: BTreeMap::default(),
2863 collection_manager,
2864 introspection_ids,
2865 introspection_tokens,
2866 now,
2867 read_only,
2868 source_statistics: Arc::new(Mutex::new(statistics::SourceStatistics {
2869 source_statistics: BTreeMap::new(),
2870 webhook_statistics: BTreeMap::new(),
2871 })),
2872 sink_statistics: Arc::new(Mutex::new(BTreeMap::new())),
2873 statistics_interval_sender,
2874 instances: BTreeMap::new(),
2875 initialized: false,
2876 config: StorageConfiguration::new(connection_context, mz_dyncfgs::all_dyncfgs()),
2877 persist_location,
2878 persist: persist_clients,
2879 metrics,
2880 recorded_frontiers: BTreeMap::new(),
2881 recorded_replica_frontiers: BTreeMap::new(),
2882 wallclock_lag,
2883 wallclock_lag_last_recorded: now_dt,
2884 storage_collections,
2885 migrated_storage_collections: BTreeSet::new(),
2886 maintenance_ticker,
2887 maintenance_scheduled: false,
2888 instance_response_rx,
2889 instance_response_tx,
2890 persist_warm_task,
2891 }
2892 }
2893
2894 #[instrument(level = "debug")]
2902 fn set_hold_policies(&mut self, policies: Vec<(GlobalId, ReadPolicy)>) {
2903 let mut read_capability_changes = BTreeMap::default();
2904
2905 for (id, policy) in policies.into_iter() {
2906 if let Some(collection) = self.collections.get_mut(&id) {
2907 let (write_frontier, derived_since, hold_policy) = match &mut collection.extra_state
2908 {
2909 CollectionStateExtra::Ingestion(ingestion) => (
2910 ingestion.write_frontier.borrow(),
2911 &mut ingestion.derived_since,
2912 &mut ingestion.hold_policy,
2913 ),
2914 CollectionStateExtra::None => {
2915 unreachable!("set_hold_policies is only called for ingestions");
2916 }
2917 CollectionStateExtra::Export(export) => (
2918 export.write_frontier.borrow(),
2919 &mut export.derived_since,
2920 &mut export.read_policy,
2921 ),
2922 };
2923
2924 let new_derived_since = policy.frontier(write_frontier);
2925 let mut update = swap_updates(derived_since, new_derived_since);
2926 if !update.is_empty() {
2927 read_capability_changes.insert(id, update);
2928 }
2929
2930 *hold_policy = policy;
2931 }
2932 }
2933
2934 if !read_capability_changes.is_empty() {
2935 self.update_hold_capabilities(&mut read_capability_changes);
2936 }
2937 }
2938
2939 #[instrument(level = "debug", fields(updates))]
2940 fn update_write_frontier(&mut self, id: GlobalId, new_upper: &Antichain<Timestamp>) {
2941 let mut read_capability_changes = BTreeMap::default();
2942
2943 if let Some(collection) = self.collections.get_mut(&id) {
2944 let (write_frontier, derived_since, hold_policy) = match &mut collection.extra_state {
2945 CollectionStateExtra::Ingestion(ingestion) => (
2946 &mut ingestion.write_frontier,
2947 &mut ingestion.derived_since,
2948 &ingestion.hold_policy,
2949 ),
2950 CollectionStateExtra::None => {
2951 if matches!(collection.data_source, DataSource::Progress) {
2952 } else {
2954 tracing::error!(
2955 ?collection,
2956 ?new_upper,
2957 "updated write frontier for collection which is not an ingestion"
2958 );
2959 }
2960 return;
2961 }
2962 CollectionStateExtra::Export(export) => (
2963 &mut export.write_frontier,
2964 &mut export.derived_since,
2965 &export.read_policy,
2966 ),
2967 };
2968
2969 if PartialOrder::less_than(write_frontier, new_upper) {
2970 write_frontier.clone_from(new_upper);
2971 }
2972
2973 let new_derived_since = hold_policy.frontier(write_frontier.borrow());
2974 let mut update = swap_updates(derived_since, new_derived_since);
2975 if !update.is_empty() {
2976 read_capability_changes.insert(id, update);
2977 }
2978 } else if self.dropped_objects.contains_key(&id) {
2979 } else {
2982 soft_panic_or_log!("spurious upper update for {id}: {new_upper:?}");
2983 }
2984
2985 if !read_capability_changes.is_empty() {
2986 self.update_hold_capabilities(&mut read_capability_changes);
2987 }
2988 }
2989
2990 #[instrument(level = "debug", fields(updates))]
2994 fn update_hold_capabilities(
2995 &mut self,
2996 updates: &mut BTreeMap<GlobalId, ChangeBatch<Timestamp>>,
2997 ) {
2998 let mut collections_net = BTreeMap::new();
3000
3001 while let Some(key) = updates.keys().rev().next().cloned() {
3006 let mut update = updates.remove(&key).unwrap();
3007
3008 if key.is_user() {
3009 debug!(id = %key, ?update, "update_hold_capability");
3010 }
3011
3012 if let Some(collection) = self.collections.get_mut(&key) {
3013 match &mut collection.extra_state {
3014 CollectionStateExtra::Ingestion(ingestion) => {
3015 let changes = ingestion.read_capabilities.update_iter(update.drain());
3016 update.extend(changes);
3017
3018 let (changes, frontier, _cluster_id) =
3019 collections_net.entry(key).or_insert_with(|| {
3020 (
3021 <ChangeBatch<_>>::new(),
3022 Antichain::new(),
3023 ingestion.instance_id,
3024 )
3025 });
3026
3027 changes.extend(update.drain());
3028 *frontier = ingestion.read_capabilities.frontier().to_owned();
3029 }
3030 CollectionStateExtra::None => {
3031 soft_panic_or_log!(
3033 "trying to update holds for collection {collection:?} which is not \
3034 an ingestion: {update:?}"
3035 );
3036 continue;
3037 }
3038 CollectionStateExtra::Export(export) => {
3039 let changes = export.read_capabilities.update_iter(update.drain());
3040 update.extend(changes);
3041
3042 let (changes, frontier, _cluster_id) =
3043 collections_net.entry(key).or_insert_with(|| {
3044 (<ChangeBatch<_>>::new(), Antichain::new(), export.cluster_id)
3045 });
3046
3047 changes.extend(update.drain());
3048 *frontier = export.read_capabilities.frontier().to_owned();
3049 }
3050 }
3051 } else {
3052 tracing::warn!(id = ?key, ?update, "update_hold_capabilities for unknown object");
3054 }
3055 }
3056
3057 for (key, (mut changes, frontier, cluster_id)) in collections_net {
3060 if !changes.is_empty() {
3061 if key.is_user() {
3062 debug!(id = %key, ?frontier, "downgrading ingestion read holds!");
3063 }
3064
3065 let collection = self
3066 .collections
3067 .get_mut(&key)
3068 .expect("missing collection state");
3069
3070 let read_holds = match &mut collection.extra_state {
3071 CollectionStateExtra::Ingestion(ingestion) => {
3072 ingestion.dependency_read_holds.as_mut_slice()
3073 }
3074 CollectionStateExtra::Export(export) => export.read_holds.as_mut_slice(),
3075 CollectionStateExtra::None => {
3076 soft_panic_or_log!(
3077 "trying to downgrade read holds for collection which is not an \
3078 ingestion: {collection:?}"
3079 );
3080 continue;
3081 }
3082 };
3083
3084 for read_hold in read_holds.iter_mut() {
3085 read_hold
3086 .try_downgrade(frontier.clone())
3087 .expect("we only advance the frontier");
3088 }
3089
3090 if let Some(instance) = self.instances.get_mut(&cluster_id) {
3092 instance.send(StorageCommand::AllowCompaction(key, frontier.clone()));
3093 } else {
3094 soft_panic_or_log!(
3095 "missing instance client for cluster {cluster_id} while we still have outstanding AllowCompaction command {frontier:?} for {key}"
3096 );
3097 }
3098 }
3099 }
3100 }
3101
3102 fn validate_collection_ids(
3104 &self,
3105 ids: impl Iterator<Item = GlobalId>,
3106 ) -> Result<(), StorageError> {
3107 for id in ids {
3108 self.storage_collections.check_exists(id)?;
3109 }
3110 Ok(())
3111 }
3112
3113 fn validate_export_ids(&self, ids: impl Iterator<Item = GlobalId>) -> Result<(), StorageError> {
3115 for id in ids {
3116 self.export(id)?;
3117 }
3118 Ok(())
3119 }
3120
3121 async fn open_data_handles(
3123 &self,
3124 id: &GlobalId,
3125 shard: ShardId,
3126 relation_desc: RelationDesc,
3127 persist_client: &PersistClient,
3128 ) -> WriteHandle<SourceData, (), Timestamp, StorageDiff> {
3129 let diagnostics = Diagnostics {
3130 shard_name: id.to_string(),
3131 handle_purpose: format!("controller data for {}", id),
3132 };
3133
3134 let mut write = persist_client
3135 .open_writer(
3136 shard,
3137 Arc::new(relation_desc),
3138 Arc::new(UnitSchema),
3139 diagnostics.clone(),
3140 )
3141 .await
3142 .expect("invalid persist usage");
3143
3144 write.fetch_recent_upper().await;
3146
3147 write
3148 }
3149
3150 fn register_introspection_collection(
3155 &mut self,
3156 id: GlobalId,
3157 introspection_type: IntrospectionType,
3158 write_handle: WriteHandle<SourceData, (), Timestamp, StorageDiff>,
3159 persist_client: PersistClient,
3160 ) -> Result<(), StorageError> {
3161 tracing::info!(%id, ?introspection_type, "registering introspection collection");
3162
3163 let force_writable = self.read_only && self.migrated_storage_collections.contains(&id);
3167 if force_writable {
3168 assert!(id.is_system(), "unexpected non-system global id: {id:?}");
3169 info!("writing to migrated storage collection {id} in read-only mode");
3170 }
3171
3172 let prev = self.introspection_ids.insert(introspection_type, id);
3173 assert!(
3174 prev.is_none(),
3175 "cannot have multiple IDs for introspection type"
3176 );
3177
3178 let metadata = self.storage_collections.collection_metadata(id)?.clone();
3179
3180 let read_handle_fn = move || {
3181 let persist_client = persist_client.clone();
3182 let metadata = metadata.clone();
3183
3184 let fut = async move {
3185 let read_handle = persist_client
3186 .open_leased_reader::<SourceData, (), Timestamp, StorageDiff>(
3187 metadata.data_shard,
3188 Arc::new(metadata.relation_desc.clone()),
3189 Arc::new(UnitSchema),
3190 Diagnostics {
3191 shard_name: id.to_string(),
3192 handle_purpose: format!("snapshot {}", id),
3193 },
3194 USE_CRITICAL_SINCE_SNAPSHOT.get(persist_client.dyncfgs()),
3195 )
3196 .await
3197 .expect("invalid persist usage");
3198 read_handle
3199 };
3200
3201 fut.boxed()
3202 };
3203
3204 let recent_upper = write_handle.shared_upper();
3205
3206 match CollectionManagerKind::from(&introspection_type) {
3207 CollectionManagerKind::Differential => {
3212 let statistics_retention_duration =
3213 dyncfgs::STATISTICS_RETENTION_DURATION.get(self.config().config_set());
3214
3215 let introspection_config = DifferentialIntrospectionConfig {
3217 recent_upper,
3218 introspection_type,
3219 storage_collections: Arc::clone(&self.storage_collections),
3220 collection_manager: self.collection_manager.clone(),
3221 source_statistics: Arc::clone(&self.source_statistics),
3222 sink_statistics: Arc::clone(&self.sink_statistics),
3223 statistics_interval: self.config.parameters.statistics_interval.clone(),
3224 statistics_interval_receiver: self.statistics_interval_sender.subscribe(),
3225 statistics_retention_duration,
3226 metrics: self.metrics.clone(),
3227 introspection_tokens: Arc::clone(&self.introspection_tokens),
3228 };
3229 self.collection_manager.register_differential_collection(
3230 id,
3231 write_handle,
3232 read_handle_fn,
3233 force_writable,
3234 introspection_config,
3235 );
3236 }
3237 CollectionManagerKind::AppendOnly => {
3245 let introspection_config = AppendOnlyIntrospectionConfig {
3246 introspection_type,
3247 config_set: Arc::clone(self.config.config_set()),
3248 parameters: self.config.parameters.clone(),
3249 storage_collections: Arc::clone(&self.storage_collections),
3250 };
3251 self.collection_manager.register_append_only_collection(
3252 id,
3253 write_handle,
3254 force_writable,
3255 Some(introspection_config),
3256 );
3257 }
3258 }
3259
3260 Ok(())
3261 }
3262
3263 fn reconcile_dangling_statistics(&self) {
3266 self.source_statistics
3267 .lock()
3268 .expect("poisoned")
3269 .source_statistics
3270 .retain(|(k, _replica_id), _| self.storage_collections.check_exists(*k).is_ok());
3272 self.sink_statistics
3273 .lock()
3274 .expect("poisoned")
3275 .retain(|(k, _replica_id), _| self.export(*k).is_ok());
3276 }
3277
3278 #[instrument(level = "debug")]
3288 fn append_shard_mappings<I>(&self, global_ids: I, diff: Diff)
3289 where
3290 I: Iterator<Item = GlobalId>,
3291 {
3292 mz_ore::soft_assert_or_log!(
3293 diff == Diff::MINUS_ONE || diff == Diff::ONE,
3294 "use 1 for insert or -1 for delete"
3295 );
3296
3297 let id = *self
3298 .introspection_ids
3299 .get(&IntrospectionType::ShardMapping)
3300 .expect("should be registered before this call");
3301
3302 let mut updates = vec![];
3303 let mut row_buf = Row::default();
3305
3306 for global_id in global_ids {
3307 let shard_id = if let Some(collection) = self.collections.get(&global_id) {
3308 collection.collection_metadata.data_shard.clone()
3309 } else {
3310 panic!("unknown global id: {}", global_id);
3311 };
3312
3313 let mut packer = row_buf.packer();
3314 packer.push(Datum::from(global_id.to_string().as_str()));
3315 packer.push(Datum::from(shard_id.to_string().as_str()));
3316 updates.push((row_buf.clone(), diff));
3317 }
3318
3319 self.collection_manager.differential_append(id, updates);
3320 }
3321
3322 fn determine_collection_dependencies(
3324 &self,
3325 self_id: GlobalId,
3326 collection_desc: &CollectionDescription,
3327 ) -> Result<Vec<GlobalId>, StorageError> {
3328 let mut dependencies = Vec::new();
3329
3330 if let Some(id) = collection_desc.primary {
3331 dependencies.push(id);
3332 }
3333
3334 match &collection_desc.data_source {
3335 DataSource::Introspection(_)
3336 | DataSource::Webhook
3337 | DataSource::Table
3338 | DataSource::Progress
3339 | DataSource::Other => (),
3340 DataSource::IngestionExport { ingestion_id, .. } => {
3341 let source_collection = self.collection(*ingestion_id)?;
3344 let ingestion_remap_collection_id = match &source_collection.data_source {
3345 DataSource::Ingestion(ingestion) => ingestion.remap_collection_id,
3346 _ => unreachable!(
3347 "SourceExport must only refer to primary sources that already exist"
3348 ),
3349 };
3350
3351 dependencies.extend([self_id, ingestion_remap_collection_id]);
3357 }
3358 DataSource::Ingestion(ingestion) => {
3360 dependencies.push(self_id);
3365 if self_id != ingestion.remap_collection_id {
3366 dependencies.push(ingestion.remap_collection_id);
3367 }
3368 }
3369 DataSource::Sink { desc } => {
3370 dependencies.extend([self_id, desc.sink.from]);
3372 }
3373 };
3374
3375 Ok(dependencies)
3376 }
3377
3378 async fn read_handle_for_snapshot(
3379 &self,
3380 id: GlobalId,
3381 ) -> Result<ReadHandle<SourceData, (), Timestamp, StorageDiff>, StorageError> {
3382 let metadata = self.storage_collections.collection_metadata(id)?;
3383 read_handle_for_snapshot(&self.persist, id, &metadata).await
3384 }
3385
3386 fn record_status_updates(&mut self, updates: Vec<StatusUpdate>) {
3389 if self.read_only {
3390 return;
3391 }
3392
3393 let mut sink_status_updates = vec![];
3394 let mut source_status_updates = vec![];
3395
3396 for update in updates {
3397 let id = update.id;
3398 if self.export(id).is_ok() {
3399 sink_status_updates.push(update);
3400 } else if self.storage_collections.check_exists(id).is_ok() {
3401 source_status_updates.push(update);
3402 }
3403 }
3404
3405 self.append_status_introspection_updates(
3406 IntrospectionType::SourceStatusHistory,
3407 source_status_updates,
3408 );
3409 self.append_status_introspection_updates(
3410 IntrospectionType::SinkStatusHistory,
3411 sink_status_updates,
3412 );
3413 }
3414
3415 fn collection(&self, id: GlobalId) -> Result<&CollectionState, StorageError> {
3416 self.collections
3417 .get(&id)
3418 .ok_or(StorageError::IdentifierMissing(id))
3419 }
3420
3421 fn run_ingestion(&mut self, id: GlobalId) -> Result<(), StorageError> {
3424 tracing::info!(%id, "starting ingestion");
3425
3426 let collection = self.collection(id)?;
3427 let ingestion_description = match &collection.data_source {
3428 DataSource::Ingestion(i) => i.clone(),
3429 _ => {
3430 tracing::warn!("run_ingestion called on non-ingestion ID {}", id);
3431 Err(StorageError::IdentifierInvalid(id))?
3432 }
3433 };
3434
3435 let mut source_exports = BTreeMap::new();
3437 for (export_id, export) in ingestion_description.source_exports.clone() {
3438 let export_storage_metadata = self.collection(export_id)?.collection_metadata.clone();
3439 source_exports.insert(
3440 export_id,
3441 SourceExport {
3442 storage_metadata: export_storage_metadata,
3443 details: export.details,
3444 data_config: export.data_config,
3445 },
3446 );
3447 }
3448
3449 let remap_collection = self.collection(ingestion_description.remap_collection_id)?;
3450
3451 let description = IngestionDescription::<CollectionMetadata> {
3452 source_exports,
3453 remap_metadata: remap_collection.collection_metadata.clone(),
3454 desc: ingestion_description.desc.clone(),
3456 instance_id: ingestion_description.instance_id,
3457 remap_collection_id: ingestion_description.remap_collection_id,
3458 };
3459
3460 let storage_instance_id = description.instance_id;
3461 let instance = self
3463 .instances
3464 .get_mut(&storage_instance_id)
3465 .ok_or_else(|| StorageError::IngestionInstanceMissing {
3466 storage_instance_id,
3467 ingestion_id: id,
3468 })?;
3469
3470 let augmented_ingestion = Box::new(RunIngestionCommand { id, description });
3471 instance.send(StorageCommand::RunIngestion(augmented_ingestion));
3472
3473 Ok(())
3474 }
3475
3476 fn run_export(&mut self, id: GlobalId) -> Result<(), StorageError> {
3479 let DataSource::Sink { desc: description } = &self.collections[&id].data_source else {
3480 return Err(StorageError::IdentifierMissing(id));
3481 };
3482
3483 let from_storage_metadata = self
3484 .storage_collections
3485 .collection_metadata(description.sink.from)?;
3486 let to_storage_metadata = self.storage_collections.collection_metadata(id)?;
3487
3488 let export_state = self.storage_collections.collection_frontiers(id)?;
3492 let mut as_of = description.sink.as_of.clone();
3493 as_of.join_assign(&export_state.implied_capability);
3494 let with_snapshot = description.sink.with_snapshot
3495 && !PartialOrder::less_than(&as_of, &export_state.write_frontier);
3496
3497 info!(
3498 sink_id = %id,
3499 from_id = %description.sink.from,
3500 write_frontier = ?export_state.write_frontier,
3501 ?as_of,
3502 ?with_snapshot,
3503 "run_export"
3504 );
3505
3506 let cmd = RunSinkCommand {
3507 id,
3508 description: StorageSinkDesc {
3509 from: description.sink.from,
3510 from_desc: description.sink.from_desc.clone(),
3511 connection: description.sink.connection.clone(),
3512 envelope: description.sink.envelope,
3513 as_of,
3514 version: description.sink.version,
3515 from_storage_metadata,
3516 with_snapshot,
3517 to_storage_metadata,
3518 commit_interval: description.sink.commit_interval,
3519 },
3520 };
3521
3522 let storage_instance_id = description.instance_id.clone();
3523
3524 let instance = self
3525 .instances
3526 .get_mut(&storage_instance_id)
3527 .ok_or_else(|| StorageError::ExportInstanceMissing {
3528 storage_instance_id,
3529 export_id: id,
3530 })?;
3531
3532 instance.send(StorageCommand::RunSink(Box::new(cmd)));
3533
3534 Ok(())
3535 }
3536
3537 fn update_frontier_introspection(&mut self) {
3542 let mut global_frontiers = BTreeMap::new();
3543 let mut replica_frontiers = BTreeMap::new();
3544
3545 for collection_frontiers in self.storage_collections.active_collection_frontiers() {
3546 let id = collection_frontiers.id;
3547 let since = collection_frontiers.read_capabilities;
3548 let upper = collection_frontiers.write_frontier;
3549
3550 let instance = self
3551 .collections
3552 .get(&id)
3553 .and_then(|collection_state| match &collection_state.extra_state {
3554 CollectionStateExtra::Ingestion(ingestion) => Some(ingestion.instance_id),
3555 CollectionStateExtra::Export(export) => Some(export.cluster_id()),
3556 CollectionStateExtra::None => None,
3557 })
3558 .and_then(|i| self.instances.get(&i));
3559
3560 if let Some(instance) = instance {
3561 for replica_id in instance.replica_ids() {
3562 replica_frontiers.insert((id, replica_id), upper.clone());
3563 }
3564 }
3565
3566 global_frontiers.insert(id, (since, upper));
3567 }
3568
3569 let mut global_updates = Vec::new();
3570 let mut replica_updates = Vec::new();
3571
3572 let mut push_global_update =
3573 |id: GlobalId,
3574 (since, upper): (Antichain<Timestamp>, Antichain<Timestamp>),
3575 diff: Diff| {
3576 let read_frontier = since.into_option().map_or(Datum::Null, |t| t.into());
3577 let write_frontier = upper.into_option().map_or(Datum::Null, |t| t.into());
3578 let row = Row::pack_slice(&[
3579 Datum::String(&id.to_string()),
3580 read_frontier,
3581 write_frontier,
3582 ]);
3583 global_updates.push((row, diff));
3584 };
3585
3586 let mut push_replica_update =
3587 |(id, replica_id): (GlobalId, ReplicaId), upper: Antichain<Timestamp>, diff: Diff| {
3588 let write_frontier = upper.into_option().map_or(Datum::Null, |t| t.into());
3589 let row = Row::pack_slice(&[
3590 Datum::String(&id.to_string()),
3591 Datum::String(&replica_id.to_string()),
3592 write_frontier,
3593 ]);
3594 replica_updates.push((row, diff));
3595 };
3596
3597 let mut old_global_frontiers =
3598 std::mem::replace(&mut self.recorded_frontiers, global_frontiers);
3599 for (&id, new) in &self.recorded_frontiers {
3600 match old_global_frontiers.remove(&id) {
3601 Some(old) if &old != new => {
3602 push_global_update(id, new.clone(), Diff::ONE);
3603 push_global_update(id, old, Diff::MINUS_ONE);
3604 }
3605 Some(_) => (),
3606 None => push_global_update(id, new.clone(), Diff::ONE),
3607 }
3608 }
3609 for (id, old) in old_global_frontiers {
3610 push_global_update(id, old, Diff::MINUS_ONE);
3611 }
3612
3613 let mut old_replica_frontiers =
3614 std::mem::replace(&mut self.recorded_replica_frontiers, replica_frontiers);
3615 for (&key, new) in &self.recorded_replica_frontiers {
3616 match old_replica_frontiers.remove(&key) {
3617 Some(old) if &old != new => {
3618 push_replica_update(key, new.clone(), Diff::ONE);
3619 push_replica_update(key, old, Diff::MINUS_ONE);
3620 }
3621 Some(_) => (),
3622 None => push_replica_update(key, new.clone(), Diff::ONE),
3623 }
3624 }
3625 for (key, old) in old_replica_frontiers {
3626 push_replica_update(key, old, Diff::MINUS_ONE);
3627 }
3628
3629 let id = self.introspection_ids[&IntrospectionType::Frontiers];
3630 self.collection_manager
3631 .differential_append(id, global_updates);
3632
3633 let id = self.introspection_ids[&IntrospectionType::ReplicaFrontiers];
3634 self.collection_manager
3635 .differential_append(id, replica_updates);
3636 }
3637
3638 fn refresh_wallclock_lag(&mut self) {
3657 let now_ms = (self.now)();
3658 let histogram_period =
3659 WallclockLagHistogramPeriod::from_epoch_millis(now_ms, self.config.config_set());
3660
3661 let frontier_lag = |frontier: &Antichain<Timestamp>| match frontier.as_option() {
3662 Some(ts) => (self.wallclock_lag)(*ts),
3663 None => Duration::ZERO,
3664 };
3665
3666 for frontiers in self.storage_collections.active_collection_frontiers() {
3667 let id = frontiers.id;
3668 let Some(collection) = self.collections.get_mut(&id) else {
3669 continue;
3670 };
3671
3672 let collection_unreadable =
3673 PartialOrder::less_equal(&frontiers.write_frontier, &frontiers.read_capabilities);
3674 let lag = if collection_unreadable {
3675 WallclockLag::Undefined
3676 } else {
3677 let lag = frontier_lag(&frontiers.write_frontier);
3678 WallclockLag::Seconds(lag.as_secs())
3679 };
3680
3681 collection.wallclock_lag_max = collection.wallclock_lag_max.max(lag);
3682
3683 let secs = lag.unwrap_seconds_or(u64::MAX);
3686 collection.wallclock_lag_metrics.observe(secs);
3687
3688 if let Some(stash) = &mut collection.wallclock_lag_histogram_stash {
3689 let bucket = lag.map_seconds(|secs| secs.next_power_of_two());
3690
3691 let instance_id = match &collection.extra_state {
3692 CollectionStateExtra::Ingestion(i) => Some(i.instance_id),
3693 CollectionStateExtra::Export(e) => Some(e.cluster_id()),
3694 CollectionStateExtra::None => None,
3695 };
3696 let workload_class = instance_id
3697 .and_then(|id| self.instances.get(&id))
3698 .and_then(|i| i.workload_class.clone());
3699 let labels = match workload_class {
3700 Some(wc) => [("workload_class", wc.clone())].into(),
3701 None => BTreeMap::new(),
3702 };
3703
3704 let key = (histogram_period, bucket, labels);
3705 *stash.entry(key).or_default() += Diff::ONE;
3706 }
3707 }
3708
3709 self.maybe_record_wallclock_lag();
3711 }
3712
3713 fn maybe_record_wallclock_lag(&mut self) {
3721 if self.read_only {
3722 return;
3723 }
3724
3725 let duration_trunc = |datetime: DateTime<_>, interval| {
3726 let td = TimeDelta::from_std(interval).ok()?;
3727 datetime.duration_trunc(td).ok()
3728 };
3729
3730 let interval = WALLCLOCK_LAG_RECORDING_INTERVAL.get(self.config.config_set());
3731 let now_dt = mz_ore::now::to_datetime((self.now)());
3732 let now_trunc = duration_trunc(now_dt, interval).unwrap_or_else(|| {
3733 soft_panic_or_log!("excessive wallclock lag recording interval: {interval:?}");
3734 let default = WALLCLOCK_LAG_RECORDING_INTERVAL.default();
3735 duration_trunc(now_dt, *default).unwrap()
3736 });
3737 if now_trunc <= self.wallclock_lag_last_recorded {
3738 return;
3739 }
3740
3741 let now_ts: CheckedTimestamp<_> = now_trunc.try_into().expect("must fit");
3742
3743 let mut history_updates = Vec::new();
3744 let mut histogram_updates = Vec::new();
3745 let mut row_buf = Row::default();
3746 for frontiers in self.storage_collections.active_collection_frontiers() {
3747 let id = frontiers.id;
3748 let Some(collection) = self.collections.get_mut(&id) else {
3749 continue;
3750 };
3751
3752 let max_lag = std::mem::replace(&mut collection.wallclock_lag_max, WallclockLag::MIN);
3753 let row = Row::pack_slice(&[
3754 Datum::String(&id.to_string()),
3755 Datum::Null,
3756 max_lag.into_interval_datum(),
3757 Datum::TimestampTz(now_ts),
3758 ]);
3759 history_updates.push((row, Diff::ONE));
3760
3761 let Some(stash) = &mut collection.wallclock_lag_histogram_stash else {
3762 continue;
3763 };
3764
3765 for ((period, lag, labels), count) in std::mem::take(stash) {
3766 let mut packer = row_buf.packer();
3767 packer.extend([
3768 Datum::TimestampTz(period.start),
3769 Datum::TimestampTz(period.end),
3770 Datum::String(&id.to_string()),
3771 lag.into_uint64_datum(),
3772 ]);
3773 let labels = labels.iter().map(|(k, v)| (*k, Datum::String(v)));
3774 packer.push_dict(labels);
3775
3776 histogram_updates.push((row_buf.clone(), count));
3777 }
3778 }
3779
3780 if !history_updates.is_empty() {
3781 self.append_introspection_updates(
3782 IntrospectionType::WallclockLagHistory,
3783 history_updates,
3784 );
3785 }
3786 if !histogram_updates.is_empty() {
3787 self.append_introspection_updates(
3788 IntrospectionType::WallclockLagHistogram,
3789 histogram_updates,
3790 );
3791 }
3792
3793 self.wallclock_lag_last_recorded = now_trunc;
3794 }
3795
3796 fn maintain(&mut self) {
3801 self.update_frontier_introspection();
3802 self.refresh_wallclock_lag();
3803
3804 for instance in self.instances.values_mut() {
3806 instance.refresh_state_metrics();
3807 }
3808 }
3809}
3810
3811impl From<&IntrospectionType> for CollectionManagerKind {
3812 fn from(value: &IntrospectionType) -> Self {
3813 match value {
3814 IntrospectionType::ShardMapping
3815 | IntrospectionType::Frontiers
3816 | IntrospectionType::ReplicaFrontiers
3817 | IntrospectionType::StorageSourceStatistics
3818 | IntrospectionType::StorageSinkStatistics
3819 | IntrospectionType::ComputeDependencies
3820 | IntrospectionType::ComputeOperatorHydrationStatus
3821 | IntrospectionType::ComputeMaterializedViewRefreshes
3822 | IntrospectionType::ComputeErrorCounts
3823 | IntrospectionType::ComputeHydrationTimes
3824 | IntrospectionType::ComputeObjectArrangementSizes => {
3825 CollectionManagerKind::Differential
3826 }
3827
3828 IntrospectionType::SourceStatusHistory
3829 | IntrospectionType::SinkStatusHistory
3830 | IntrospectionType::PrivatelinkConnectionStatusHistory
3831 | IntrospectionType::ReplicaStatusHistory
3832 | IntrospectionType::ReplicaMetricsHistory
3833 | IntrospectionType::WallclockLagHistory
3834 | IntrospectionType::WallclockLagHistogram
3835 | IntrospectionType::PreparedStatementHistory
3836 | IntrospectionType::StatementExecutionHistory
3837 | IntrospectionType::SessionHistory
3838 | IntrospectionType::StatementLifecycleHistory
3839 | IntrospectionType::SqlText => CollectionManagerKind::AppendOnly,
3840 }
3841 }
3842}
3843
3844async fn snapshot_statistics(
3850 id: GlobalId,
3851 upper: Antichain<Timestamp>,
3852 storage_collections: &Arc<dyn StorageCollections + Send + Sync>,
3853) -> Vec<Row> {
3854 match upper.as_option() {
3855 Some(f) if f > &Timestamp::MIN => {
3856 let as_of = f.step_back().unwrap();
3857
3858 let snapshot = storage_collections.snapshot(id, as_of).await.unwrap();
3859 snapshot
3860 .into_iter()
3861 .map(|(row, diff)| {
3862 assert_eq!(diff, 1);
3863 row
3864 })
3865 .collect()
3866 }
3867 _ => Vec::new(),
3870 }
3871}
3872
3873async fn read_handle_for_snapshot(
3874 persist: &PersistClientCache,
3875 id: GlobalId,
3876 metadata: &CollectionMetadata,
3877) -> Result<ReadHandle<SourceData, (), Timestamp, StorageDiff>, StorageError> {
3878 let persist_client = persist
3879 .open(metadata.persist_location.clone())
3880 .await
3881 .unwrap();
3882
3883 let read_handle = persist_client
3888 .open_leased_reader::<SourceData, (), _, _>(
3889 metadata.data_shard,
3890 Arc::new(metadata.relation_desc.clone()),
3891 Arc::new(UnitSchema),
3892 Diagnostics {
3893 shard_name: id.to_string(),
3894 handle_purpose: format!("snapshot {}", id),
3895 },
3896 USE_CRITICAL_SINCE_SNAPSHOT.get(persist_client.dyncfgs()),
3897 )
3898 .await
3899 .expect("invalid persist usage");
3900 Ok(read_handle)
3901}
3902
3903#[derive(Debug)]
3905struct CollectionState {
3906 pub data_source: DataSource,
3908
3909 pub collection_metadata: CollectionMetadata,
3910
3911 pub extra_state: CollectionStateExtra,
3912
3913 wallclock_lag_max: WallclockLag,
3915 wallclock_lag_histogram_stash: Option<
3922 BTreeMap<
3923 (
3924 WallclockLagHistogramPeriod,
3925 WallclockLag,
3926 BTreeMap<&'static str, String>,
3927 ),
3928 Diff,
3929 >,
3930 >,
3931 wallclock_lag_metrics: WallclockLagMetrics,
3933}
3934
3935impl CollectionState {
3936 fn new(
3937 data_source: DataSource,
3938 collection_metadata: CollectionMetadata,
3939 extra_state: CollectionStateExtra,
3940 wallclock_lag_metrics: WallclockLagMetrics,
3941 ) -> Self {
3942 let wallclock_lag_histogram_stash = match &data_source {
3946 DataSource::Other => None,
3947 _ => Some(Default::default()),
3948 };
3949
3950 Self {
3951 data_source,
3952 collection_metadata,
3953 extra_state,
3954 wallclock_lag_max: WallclockLag::MIN,
3955 wallclock_lag_histogram_stash,
3956 wallclock_lag_metrics,
3957 }
3958 }
3959}
3960
3961#[derive(Debug)]
3963enum CollectionStateExtra {
3964 Ingestion(IngestionState),
3965 Export(ExportState),
3966 None,
3967}
3968
3969#[derive(Debug)]
3971struct IngestionState {
3972 pub read_capabilities: MutableAntichain<Timestamp>,
3974
3975 pub derived_since: Antichain<Timestamp>,
3978
3979 pub dependency_read_holds: Vec<ReadHold>,
3981
3982 pub write_frontier: Antichain<Timestamp>,
3984
3985 pub hold_policy: ReadPolicy,
3992
3993 pub instance_id: StorageInstanceId,
3995
3996 pub hydrated_on: BTreeSet<ReplicaId>,
3998}
3999
4000struct StatusHistoryDesc<K> {
4005 retention_policy: StatusHistoryRetentionPolicy,
4006 extract_key: Box<dyn Fn(&[Datum]) -> K + Send>,
4007 extract_time: Box<dyn Fn(&[Datum]) -> CheckedTimestamp<DateTime<Utc>> + Send>,
4008}
4009enum StatusHistoryRetentionPolicy {
4010 LastN(usize),
4012 TimeWindow(Duration),
4014}
4015
4016fn source_status_history_desc(
4017 params: &StorageParameters,
4018) -> StatusHistoryDesc<(GlobalId, Option<ReplicaId>)> {
4019 let desc = &MZ_SOURCE_STATUS_HISTORY_DESC;
4020 let (source_id_idx, _) = desc.get_by_name(&"source_id".into()).expect("exists");
4021 let (replica_id_idx, _) = desc.get_by_name(&"replica_id".into()).expect("exists");
4022 let (time_idx, _) = desc.get_by_name(&"occurred_at".into()).expect("exists");
4023
4024 StatusHistoryDesc {
4025 retention_policy: StatusHistoryRetentionPolicy::LastN(
4026 params.keep_n_source_status_history_entries,
4027 ),
4028 extract_key: Box::new(move |datums| {
4029 (
4030 GlobalId::from_str(datums[source_id_idx].unwrap_str()).expect("GlobalId column"),
4031 if datums[replica_id_idx].is_null() {
4032 None
4033 } else {
4034 Some(
4035 ReplicaId::from_str(datums[replica_id_idx].unwrap_str())
4036 .expect("ReplicaId column"),
4037 )
4038 },
4039 )
4040 }),
4041 extract_time: Box::new(move |datums| datums[time_idx].unwrap_timestamptz()),
4042 }
4043}
4044
4045fn sink_status_history_desc(
4046 params: &StorageParameters,
4047) -> StatusHistoryDesc<(GlobalId, Option<ReplicaId>)> {
4048 let desc = &MZ_SINK_STATUS_HISTORY_DESC;
4049 let (sink_id_idx, _) = desc.get_by_name(&"sink_id".into()).expect("exists");
4050 let (replica_id_idx, _) = desc.get_by_name(&"replica_id".into()).expect("exists");
4051 let (time_idx, _) = desc.get_by_name(&"occurred_at".into()).expect("exists");
4052
4053 StatusHistoryDesc {
4054 retention_policy: StatusHistoryRetentionPolicy::LastN(
4055 params.keep_n_sink_status_history_entries,
4056 ),
4057 extract_key: Box::new(move |datums| {
4058 (
4059 GlobalId::from_str(datums[sink_id_idx].unwrap_str()).expect("GlobalId column"),
4060 if datums[replica_id_idx].is_null() {
4061 None
4062 } else {
4063 Some(
4064 ReplicaId::from_str(datums[replica_id_idx].unwrap_str())
4065 .expect("ReplicaId column"),
4066 )
4067 },
4068 )
4069 }),
4070 extract_time: Box::new(move |datums| datums[time_idx].unwrap_timestamptz()),
4071 }
4072}
4073
4074fn privatelink_status_history_desc(params: &StorageParameters) -> StatusHistoryDesc<GlobalId> {
4075 let desc = &MZ_AWS_PRIVATELINK_CONNECTION_STATUS_HISTORY_DESC;
4076 let (key_idx, _) = desc.get_by_name(&"connection_id".into()).expect("exists");
4077 let (time_idx, _) = desc.get_by_name(&"occurred_at".into()).expect("exists");
4078
4079 StatusHistoryDesc {
4080 retention_policy: StatusHistoryRetentionPolicy::LastN(
4081 params.keep_n_privatelink_status_history_entries,
4082 ),
4083 extract_key: Box::new(move |datums| {
4084 GlobalId::from_str(datums[key_idx].unwrap_str()).expect("GlobalId column")
4085 }),
4086 extract_time: Box::new(move |datums| datums[time_idx].unwrap_timestamptz()),
4087 }
4088}
4089
4090fn replica_status_history_desc(params: &StorageParameters) -> StatusHistoryDesc<(GlobalId, u64)> {
4091 let desc = &REPLICA_STATUS_HISTORY_DESC;
4092 let (replica_idx, _) = desc.get_by_name(&"replica_id".into()).expect("exists");
4093 let (process_idx, _) = desc.get_by_name(&"process_id".into()).expect("exists");
4094 let (time_idx, _) = desc.get_by_name(&"occurred_at".into()).expect("exists");
4095
4096 StatusHistoryDesc {
4097 retention_policy: StatusHistoryRetentionPolicy::TimeWindow(
4098 params.replica_status_history_retention_window,
4099 ),
4100 extract_key: Box::new(move |datums| {
4101 (
4102 GlobalId::from_str(datums[replica_idx].unwrap_str()).expect("GlobalId column"),
4103 datums[process_idx].unwrap_uint64(),
4104 )
4105 }),
4106 extract_time: Box::new(move |datums| datums[time_idx].unwrap_timestamptz()),
4107 }
4108}
4109
4110fn swap_updates(
4112 from: &mut Antichain<Timestamp>,
4113 mut replace_with: Antichain<Timestamp>,
4114) -> ChangeBatch<Timestamp> {
4115 let mut update = ChangeBatch::new();
4116 if PartialOrder::less_equal(from, &replace_with) {
4117 update.extend(replace_with.iter().map(|time| (*time, 1)));
4118 std::mem::swap(from, &mut replace_with);
4119 update.extend(replace_with.iter().map(|time| (*time, -1)));
4120 }
4121 update
4122}