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