1use std::any::Any;
13use std::collections::btree_map;
14use std::collections::{BTreeMap, BTreeSet};
15use std::fmt::Debug;
16use std::str::FromStr;
17use std::sync::{Arc, Mutex};
18use std::time::Duration;
19
20use crate::collection_mgmt::{
21 AppendOnlyIntrospectionConfig, CollectionManagerKind, DifferentialIntrospectionConfig,
22};
23use crate::instance::{Instance, ReplicaConfig};
24use async_trait::async_trait;
25use chrono::{DateTime, DurationRound, TimeDelta, Utc};
26use derivative::Derivative;
27use differential_dataflow::lattice::Lattice;
28use futures::FutureExt;
29use futures::StreamExt;
30use itertools::Itertools;
31use mz_build_info::BuildInfo;
32use mz_cluster_client::client::ClusterReplicaLocation;
33use mz_cluster_client::metrics::{ControllerMetrics, WallclockLagMetrics};
34use mz_cluster_client::{ReplicaId, WallclockLagFn};
35use mz_controller_types::dyncfgs::{
36 ENABLE_0DT_DEPLOYMENT_SOURCES, WALLCLOCK_LAG_RECORDING_INTERVAL,
37};
38use mz_ore::collections::CollectionExt;
39use mz_ore::metrics::MetricsRegistry;
40use mz_ore::now::NowFn;
41use mz_ore::task::AbortOnDropHandle;
42use mz_ore::{assert_none, halt, instrument, soft_panic_or_log};
43use mz_persist_client::batch::ProtoBatch;
44use mz_persist_client::cache::PersistClientCache;
45use mz_persist_client::cfg::USE_CRITICAL_SINCE_SNAPSHOT;
46use mz_persist_client::critical::Opaque;
47use mz_persist_client::read::ReadHandle;
48use mz_persist_client::schema::CaESchema;
49use mz_persist_client::write::WriteHandle;
50use mz_persist_client::{Diagnostics, PersistClient, PersistLocation, ShardId};
51use mz_persist_types::codec_impls::UnitSchema;
52use mz_repr::adt::timestamp::CheckedTimestamp;
53use mz_repr::{Datum, Diff, GlobalId, RelationDesc, RelationVersion, Row, Timestamp};
54use mz_storage_client::client::{
55 AppendOnlyUpdate, RunIngestionCommand, RunOneshotIngestion, RunSinkCommand, Status,
56 StatusUpdate, StorageCommand, StorageResponse, TableData,
57};
58use mz_storage_client::controller::{
59 BoxFuture, CollectionDescription, DataSource, ExportDescription, ExportState,
60 IntrospectionType, MonotonicAppender, PersistEpoch, Response, StorageController,
61 StorageMetadata, StorageTxn, StorageWriteOp, WallclockLag, WallclockLagHistogramPeriod,
62};
63use mz_storage_client::healthcheck::{
64 MZ_AWS_PRIVATELINK_CONNECTION_STATUS_HISTORY_DESC, MZ_SINK_STATUS_HISTORY_DESC,
65 MZ_SOURCE_STATUS_HISTORY_DESC, REPLICA_STATUS_HISTORY_DESC,
66};
67use mz_storage_client::metrics::StorageControllerMetrics;
68use mz_storage_client::statistics::{
69 ControllerSinkStatistics, ControllerSourceStatistics, WebhookStatistics,
70};
71use mz_storage_client::storage_collections::StorageCollections;
72use mz_storage_types::configuration::StorageConfiguration;
73use mz_storage_types::connections::ConnectionContext;
74use mz_storage_types::connections::inline::InlinedConnection;
75use mz_storage_types::controller::{AlterError, CollectionMetadata, StorageError, TxnsCodecRow};
76use mz_storage_types::errors::CollectionMissing;
77use mz_storage_types::instances::StorageInstanceId;
78use mz_storage_types::oneshot_sources::{OneshotIngestionRequest, OneshotResultCallback};
79use mz_storage_types::parameters::StorageParameters;
80use mz_storage_types::read_holds::ReadHold;
81use mz_storage_types::read_policy::ReadPolicy;
82use mz_storage_types::sinks::{StorageSinkConnection, StorageSinkDesc};
83use mz_storage_types::sources::{
84 GenericSourceConnection, IngestionDescription, SourceConnection, SourceData, SourceDesc,
85 SourceExport, SourceExportDataConfig,
86};
87use mz_storage_types::{AlterCompatible, StorageDiff, dyncfgs};
88use mz_txn_wal::metrics::Metrics as TxnMetrics;
89use mz_txn_wal::txn_read::TxnsRead;
90use mz_txn_wal::txns::TxnsHandle;
91use timely::order::PartialOrder;
92use timely::progress::frontier::MutableAntichain;
93use timely::progress::{Antichain, ChangeBatch};
94use tokio::sync::watch::{Sender, channel};
95use tokio::sync::{mpsc, oneshot};
96use tokio::time::MissedTickBehavior;
97use tokio::time::error::Elapsed;
98use tracing::{debug, info, warn};
99
100mod collection_mgmt;
101mod history;
102mod instance;
103mod persist_handles;
104mod rtr;
105mod statistics;
106
107#[derive(Derivative)]
108#[derivative(Debug)]
109struct PendingOneshotIngestion {
110 #[derivative(Debug = "ignore")]
112 result_tx: OneshotResultCallback<ProtoBatch>,
113 cluster_id: StorageInstanceId,
115}
116
117impl PendingOneshotIngestion {
118 pub(crate) fn cancel(self) {
122 (self.result_tx)(vec![Err("canceled".to_string())])
123 }
124}
125
126#[derive(Derivative)]
128#[derivative(Debug)]
129pub struct Controller {
130 build_info: &'static BuildInfo,
132 now: NowFn,
134
135 read_only: bool,
141
142 pub(crate) collections: BTreeMap<GlobalId, CollectionState>,
147
148 dropped_objects: BTreeMap<GlobalId, BTreeSet<ReplicaId>>,
157
158 pub(crate) persist_table_worker: persist_handles::PersistTableWriteWorker,
160 txns_read: TxnsRead<Timestamp>,
162 txns_metrics: Arc<TxnMetrics>,
163 stashed_responses: Vec<(Option<ReplicaId>, StorageResponse)>,
164 #[derivative(Debug = "ignore")]
166 pending_table_handle_drops_tx: mpsc::UnboundedSender<GlobalId>,
167 #[derivative(Debug = "ignore")]
169 pending_table_handle_drops_rx: mpsc::UnboundedReceiver<GlobalId>,
170 #[derivative(Debug = "ignore")]
172 pending_oneshot_ingestions: BTreeMap<uuid::Uuid, PendingOneshotIngestion>,
173
174 pub(crate) collection_manager: collection_mgmt::CollectionManager,
176
177 pub(crate) introspection_ids: BTreeMap<IntrospectionType, GlobalId>,
179 introspection_tokens: Arc<Mutex<BTreeMap<GlobalId, Box<dyn Any + Send + Sync>>>>,
184
185 source_statistics: Arc<Mutex<statistics::SourceStatistics>>,
190 sink_statistics: Arc<Mutex<BTreeMap<(GlobalId, Option<ReplicaId>), ControllerSinkStatistics>>>,
193 statistics_interval_sender: Sender<Duration>,
195
196 instances: BTreeMap<StorageInstanceId, Instance>,
198 initialized: bool,
200 config: StorageConfiguration,
202 persist_location: PersistLocation,
204 persist: Arc<PersistClientCache>,
206 metrics: StorageControllerMetrics,
208 recorded_frontiers: BTreeMap<GlobalId, (Antichain<Timestamp>, Antichain<Timestamp>)>,
211 recorded_replica_frontiers: BTreeMap<(GlobalId, ReplicaId), Antichain<Timestamp>>,
214
215 #[derivative(Debug = "ignore")]
217 wallclock_lag: WallclockLagFn<Timestamp>,
218 wallclock_lag_last_recorded: DateTime<Utc>,
220
221 storage_collections: Arc<dyn StorageCollections + Send + Sync>,
223 migrated_storage_collections: BTreeSet<GlobalId>,
225
226 maintenance_ticker: tokio::time::Interval,
228 maintenance_scheduled: bool,
230
231 instance_response_tx: mpsc::UnboundedSender<(Option<ReplicaId>, StorageResponse)>,
233 instance_response_rx: mpsc::UnboundedReceiver<(Option<ReplicaId>, StorageResponse)>,
235
236 persist_warm_task: Option<AbortOnDropHandle<Box<dyn Debug + Send>>>,
238}
239
240fn warm_persist_state_in_background(
245 client: PersistClient,
246 shard_ids: impl Iterator<Item = ShardId> + Send + 'static,
247) -> mz_ore::task::JoinHandle<Box<dyn Debug + Send>> {
248 const MAX_CONCURRENT_WARMS: usize = 16;
250 let logic = async move {
251 let fetchers: Vec<_> = tokio_stream::iter(shard_ids)
252 .map(|shard_id| {
253 let client = client.clone();
254 async move {
255 client
256 .create_batch_fetcher::<SourceData, (), mz_repr::Timestamp, StorageDiff>(
257 shard_id,
258 Arc::new(RelationDesc::empty()),
259 Arc::new(UnitSchema),
260 true,
261 Diagnostics::from_purpose("warm persist load state"),
262 )
263 .await
264 }
265 })
266 .buffer_unordered(MAX_CONCURRENT_WARMS)
267 .collect()
268 .await;
269 let fetchers: Box<dyn Debug + Send> = Box::new(fetchers);
270 fetchers
271 };
272 mz_ore::task::spawn(|| "warm_persist_load_state", logic)
273}
274
275#[async_trait(?Send)]
276impl StorageController for Controller {
277 fn initialization_complete(&mut self) {
278 self.reconcile_dangling_statistics();
279 self.initialized = true;
280
281 for instance in self.instances.values_mut() {
282 instance.send(StorageCommand::InitializationComplete);
283 }
284 }
285
286 fn update_parameters(&mut self, config_params: StorageParameters) {
287 self.storage_collections
288 .update_parameters(config_params.clone());
289
290 self.persist.cfg().apply_from(&config_params.dyncfg_updates);
293
294 for instance in self.instances.values_mut() {
295 let params = Box::new(config_params.clone());
296 instance.send(StorageCommand::UpdateConfiguration(params));
297 }
298 self.config.update(config_params);
299 self.statistics_interval_sender
300 .send_replace(self.config.parameters.statistics_interval);
301 self.collection_manager.update_user_batch_duration(
302 self.config
303 .parameters
304 .user_storage_managed_collections_batch_duration,
305 );
306 }
307
308 fn config(&self) -> &StorageConfiguration {
310 &self.config
311 }
312
313 fn collection_metadata(&self, id: GlobalId) -> Result<CollectionMetadata, CollectionMissing> {
314 self.storage_collections.collection_metadata(id)
315 }
316
317 fn collection_hydrated(&self, collection_id: GlobalId) -> Result<bool, StorageError> {
318 let collection = self.collection(collection_id)?;
319
320 let instance_id = match &collection.data_source {
321 DataSource::Ingestion(ingestion_description) => ingestion_description.instance_id,
322 DataSource::IngestionExport { ingestion_id, .. } => {
323 let ingestion_state = self.collections.get(ingestion_id).expect("known to exist");
324
325 let instance_id = match &ingestion_state.data_source {
326 DataSource::Ingestion(ingestion_desc) => ingestion_desc.instance_id,
327 _ => unreachable!("SourceExport must only refer to primary source"),
328 };
329
330 instance_id
331 }
332 _ => return Ok(true),
333 };
334
335 let instance = self.instances.get(&instance_id).ok_or_else(|| {
336 StorageError::IngestionInstanceMissing {
337 storage_instance_id: instance_id,
338 ingestion_id: collection_id,
339 }
340 })?;
341
342 if instance.replica_ids().next().is_none() {
343 return Ok(true);
346 }
347
348 match &collection.extra_state {
349 CollectionStateExtra::Ingestion(ingestion_state) => {
350 Ok(ingestion_state.hydrated_on.len() >= 1)
352 }
353 CollectionStateExtra::Export(_) => {
354 Ok(true)
359 }
360 CollectionStateExtra::None => {
361 Ok(true)
365 }
366 }
367 }
368
369 #[mz_ore::instrument(level = "debug")]
370 fn collections_hydrated_on_replicas(
371 &self,
372 target_replica_ids: Option<Vec<ReplicaId>>,
373 target_cluster_id: &StorageInstanceId,
374 exclude_collections: &BTreeSet<GlobalId>,
375 ) -> Result<bool, StorageError> {
376 if target_replica_ids.as_ref().is_some_and(|v| v.is_empty()) {
379 return Ok(true);
380 }
381
382 let target_replicas: Option<BTreeSet<ReplicaId>> =
385 target_replica_ids.map(|ids| ids.into_iter().collect());
386
387 let mut all_hydrated = true;
388 for (collection_id, collection_state) in self.collections.iter() {
389 if collection_id.is_transient() || exclude_collections.contains(collection_id) {
390 continue;
391 }
392 let hydrated = match &collection_state.extra_state {
393 CollectionStateExtra::Ingestion(state) => {
394 if &state.instance_id != target_cluster_id {
395 continue;
396 }
397 match &target_replicas {
398 Some(target_replicas) => !state.hydrated_on.is_disjoint(target_replicas),
399 None => {
400 state.hydrated_on.len() >= 1
403 }
404 }
405 }
406 CollectionStateExtra::Export(_) => {
407 true
412 }
413 CollectionStateExtra::None => {
414 true
418 }
419 };
420 if !hydrated {
421 tracing::info!(%collection_id, "collection is not hydrated on any replica");
422 all_hydrated = false;
423 }
426 }
427 Ok(all_hydrated)
428 }
429
430 fn collection_frontiers(
431 &self,
432 id: GlobalId,
433 ) -> Result<(Antichain<Timestamp>, Antichain<Timestamp>), CollectionMissing> {
434 let frontiers = self.storage_collections.collection_frontiers(id)?;
435 Ok((frontiers.implied_capability, frontiers.write_frontier))
436 }
437
438 fn collections_frontiers(
439 &self,
440 mut ids: Vec<GlobalId>,
441 ) -> Result<Vec<(GlobalId, Antichain<Timestamp>, Antichain<Timestamp>)>, CollectionMissing>
442 {
443 let mut result = vec![];
444 ids.retain(|&id| match self.export(id) {
449 Ok(export) => {
450 result.push((
451 id,
452 export.input_hold().since().clone(),
453 export.write_frontier.clone(),
454 ));
455 false
456 }
457 Err(_) => true,
458 });
459 result.extend(
460 self.storage_collections
461 .collections_frontiers(ids)?
462 .into_iter()
463 .map(|frontiers| {
464 (
465 frontiers.id,
466 frontiers.implied_capability,
467 frontiers.write_frontier,
468 )
469 }),
470 );
471
472 Ok(result)
473 }
474
475 fn active_collection_metadatas(&self) -> Vec<(GlobalId, CollectionMetadata)> {
476 self.storage_collections.active_collection_metadatas()
477 }
478
479 fn active_ingestion_exports(
480 &self,
481 instance_id: StorageInstanceId,
482 ) -> Box<dyn Iterator<Item = &GlobalId> + '_> {
483 let active_storage_collections: BTreeMap<_, _> = self
484 .storage_collections
485 .active_collection_frontiers()
486 .into_iter()
487 .map(|c| (c.id, c))
488 .collect();
489
490 let active_exports = self.instances[&instance_id]
491 .active_ingestion_exports()
492 .filter(move |id| {
493 let frontiers = active_storage_collections.get(id);
494 match frontiers {
495 Some(frontiers) => !frontiers.write_frontier.is_empty(),
496 None => {
497 false
499 }
500 }
501 });
502
503 Box::new(active_exports)
504 }
505
506 fn check_exists(&self, id: GlobalId) -> Result<(), StorageError> {
507 self.storage_collections.check_exists(id)
508 }
509
510 fn create_instance(&mut self, id: StorageInstanceId, workload_class: Option<String>) {
511 let metrics = self.metrics.for_instance(id);
512 let mut instance = Instance::new(
513 workload_class,
514 metrics,
515 self.now.clone(),
516 self.instance_response_tx.clone(),
517 );
518 if self.initialized {
519 instance.send(StorageCommand::InitializationComplete);
520 }
521 if !self.read_only {
522 instance.send(StorageCommand::AllowWrites);
523 }
524
525 let params = Box::new(self.config.parameters.clone());
526 instance.send(StorageCommand::UpdateConfiguration(params));
527
528 let old_instance = self.instances.insert(id, instance);
529 assert_none!(old_instance, "storage instance {id} already exists");
530 }
531
532 fn drop_instance(&mut self, id: StorageInstanceId) {
533 let instance = self.instances.remove(&id);
534 assert!(instance.is_some(), "storage instance {id} does not exist");
535 }
536
537 fn update_instance_workload_class(
538 &mut self,
539 id: StorageInstanceId,
540 workload_class: Option<String>,
541 ) {
542 let instance = self
543 .instances
544 .get_mut(&id)
545 .unwrap_or_else(|| panic!("instance {id} does not exist"));
546
547 instance.workload_class = workload_class;
548 }
549
550 fn connect_replica(
551 &mut self,
552 instance_id: StorageInstanceId,
553 replica_id: ReplicaId,
554 location: ClusterReplicaLocation,
555 ) {
556 let instance = self
557 .instances
558 .get_mut(&instance_id)
559 .unwrap_or_else(|| panic!("instance {instance_id} does not exist"));
560
561 let config = ReplicaConfig {
562 build_info: self.build_info,
563 location,
564 grpc_client: self.config.parameters.grpc_client.clone(),
565 };
566 instance.add_replica(replica_id, config);
567 }
568
569 fn drop_replica(&mut self, instance_id: StorageInstanceId, replica_id: ReplicaId) {
570 let instance = self
571 .instances
572 .get_mut(&instance_id)
573 .unwrap_or_else(|| panic!("instance {instance_id} does not exist"));
574
575 let status_now = mz_ore::now::to_datetime((self.now)());
576 let mut source_status_updates = vec![];
577 let mut sink_status_updates = vec![];
578
579 let make_update = |id, object_type| StatusUpdate {
582 id,
583 status: Status::Paused,
584 timestamp: status_now,
585 error: None,
586 hints: BTreeSet::from([format!(
587 "The replica running this {object_type} has been dropped"
588 )]),
589 namespaced_errors: Default::default(),
590 replica_id: Some(replica_id),
591 };
592
593 for ingestion_id in instance.active_ingestions() {
594 if let Some(active_replicas) = self.dropped_objects.get_mut(ingestion_id) {
595 active_replicas.remove(&replica_id);
596 if active_replicas.is_empty() {
597 self.dropped_objects.remove(ingestion_id);
598 }
599 }
600
601 let ingestion = self
602 .collections
603 .get_mut(ingestion_id)
604 .expect("instance contains unknown ingestion");
605
606 let ingestion_description = match &ingestion.data_source {
607 DataSource::Ingestion(ingestion_description) => ingestion_description.clone(),
608 _ => panic!(
609 "unexpected data source for ingestion: {:?}",
610 ingestion.data_source
611 ),
612 };
613
614 let old_style_ingestion = *ingestion_id != ingestion_description.remap_collection_id;
615 let subsource_ids = ingestion_description.collection_ids().filter(|id| {
616 let should_discard =
621 old_style_ingestion && id == &ingestion_description.remap_collection_id;
622 !should_discard
623 });
624 for id in subsource_ids {
625 source_status_updates.push(make_update(id, "source"));
626 }
627 }
628
629 for id in instance.active_exports() {
630 if let Some(active_replicas) = self.dropped_objects.get_mut(id) {
631 active_replicas.remove(&replica_id);
632 if active_replicas.is_empty() {
633 self.dropped_objects.remove(id);
634 }
635 }
636
637 sink_status_updates.push(make_update(*id, "sink"));
638 }
639
640 instance.drop_replica(replica_id);
641
642 if !self.read_only {
643 if !source_status_updates.is_empty() {
644 self.append_status_introspection_updates(
645 IntrospectionType::SourceStatusHistory,
646 source_status_updates,
647 );
648 }
649 if !sink_status_updates.is_empty() {
650 self.append_status_introspection_updates(
651 IntrospectionType::SinkStatusHistory,
652 sink_status_updates,
653 );
654 }
655 }
656 }
657
658 async fn evolve_nullability_for_bootstrap(
659 &mut self,
660 storage_metadata: &StorageMetadata,
661 collections: Vec<(GlobalId, RelationDesc)>,
662 ) -> Result<(), StorageError> {
663 let persist_client = self
664 .persist
665 .open(self.persist_location.clone())
666 .await
667 .unwrap();
668
669 for (global_id, relation_desc) in collections {
670 let shard_id = storage_metadata.get_collection_shard(global_id)?;
671 let diagnostics = Diagnostics {
672 shard_name: global_id.to_string(),
673 handle_purpose: "evolve nullability for bootstrap".to_string(),
674 };
675 let latest_schema = persist_client
676 .latest_schema::<SourceData, (), Timestamp, StorageDiff>(shard_id, diagnostics)
677 .await
678 .expect("invalid persist usage");
679 let Some((schema_id, current_schema, _)) = latest_schema else {
680 tracing::debug!(?global_id, "no schema registered");
681 continue;
682 };
683 tracing::debug!(?global_id, ?current_schema, new_schema = ?relation_desc, "migrating schema");
684
685 let diagnostics = Diagnostics {
686 shard_name: global_id.to_string(),
687 handle_purpose: "evolve nullability for bootstrap".to_string(),
688 };
689 let evolve_result = persist_client
690 .compare_and_evolve_schema::<SourceData, (), Timestamp, StorageDiff>(
691 shard_id,
692 schema_id,
693 &relation_desc,
694 &UnitSchema,
695 diagnostics,
696 )
697 .await
698 .expect("invalid persist usage");
699 match evolve_result {
700 CaESchema::Ok(_) => (),
701 CaESchema::ExpectedMismatch {
702 schema_id,
703 key,
704 val: _,
705 } => {
706 return Err(StorageError::PersistSchemaEvolveRace {
707 global_id,
708 shard_id,
709 schema_id,
710 relation_desc: key,
711 });
712 }
713 CaESchema::Incompatible => {
714 return Err(StorageError::PersistInvalidSchemaEvolve {
715 global_id,
716 shard_id,
717 });
718 }
719 };
720 }
721
722 Ok(())
723 }
724
725 #[instrument(name = "storage::create_collections")]
744 async fn create_collections_for_bootstrap(
745 &mut self,
746 storage_metadata: &StorageMetadata,
747 register_ts: Option<Timestamp>,
748 mut collections: Vec<(GlobalId, CollectionDescription)>,
749 migrated_storage_collections: &BTreeSet<GlobalId>,
750 ) -> Result<(), StorageError> {
751 self.migrated_storage_collections
752 .extend(migrated_storage_collections.iter().cloned());
753
754 self.storage_collections
755 .create_collections_for_bootstrap(
756 storage_metadata,
757 register_ts,
758 collections.clone(),
759 migrated_storage_collections,
760 )
761 .await?;
762
763 drop(self.persist_warm_task.take());
766
767 collections.sort_by_key(|(id, _)| *id);
772 collections.dedup();
773 for pos in 1..collections.len() {
774 if collections[pos - 1].0 == collections[pos].0 {
775 return Err(StorageError::CollectionIdReused(collections[pos].0));
776 }
777 }
778
779 let enriched_with_metadata = collections
781 .into_iter()
782 .map(|(id, description)| {
783 let data_shard = storage_metadata.get_collection_shard(id)?;
784
785 let txns_shard = description
788 .data_source
789 .in_txns()
790 .then(|| *self.txns_read.txns_id());
791
792 let metadata = CollectionMetadata {
793 persist_location: self.persist_location.clone(),
794 data_shard,
795 relation_desc: description.desc.clone(),
796 txns_shard,
797 };
798
799 Ok((id, description, metadata))
800 })
801 .collect_vec();
802
803 let persist_client = self
805 .persist
806 .open(self.persist_location.clone())
807 .await
808 .unwrap();
809 let persist_client = &persist_client;
810
811 use futures::stream::{StreamExt, TryStreamExt};
814 let this = &*self;
815 let mut to_register: Vec<_> = futures::stream::iter(enriched_with_metadata)
816 .map(|data: Result<_, StorageError>| {
817 async move {
818 let (id, description, metadata) = data?;
819
820 debug!("mapping GlobalId={} to shard ({})", id, metadata.data_shard);
823
824 let write = this
825 .open_data_handles(
826 &id,
827 metadata.data_shard,
828 metadata.relation_desc.clone(),
829 persist_client,
830 )
831 .await;
832
833 Ok::<_, StorageError>((id, description, write, metadata))
834 }
835 })
836 .buffer_unordered(50)
838 .try_collect()
851 .await?;
852
853 let mut to_execute = BTreeSet::new();
856 let mut new_collections = BTreeSet::new();
861 let mut table_registers = Vec::with_capacity(to_register.len());
862
863 to_register.sort_by_key(|(id, ..)| *id);
865
866 let (tables_to_register, collections_to_register): (Vec<_>, Vec<_>) = to_register
872 .into_iter()
873 .partition(|(_id, desc, ..)| desc.data_source == DataSource::Table);
874 let to_register = tables_to_register
875 .into_iter()
876 .rev()
877 .chain(collections_to_register.into_iter());
878
879 let mut new_webhook_statistic_entries = BTreeSet::new();
883
884 for (id, description, write, metadata) in to_register {
885 let is_in_txns = |id, metadata: &CollectionMetadata| {
886 metadata.txns_shard.is_some()
887 && !(self.read_only && migrated_storage_collections.contains(&id))
888 };
889
890 to_execute.insert(id);
891 new_collections.insert(id);
892
893 let write_frontier = write.upper();
894
895 let storage_dependencies = self.determine_collection_dependencies(id, &description)?;
897
898 let dependency_read_holds = self
899 .storage_collections
900 .acquire_read_holds(storage_dependencies)
901 .expect("can acquire read holds");
902
903 let mut dependency_since = Antichain::from_elem(Timestamp::MIN);
904 for read_hold in dependency_read_holds.iter() {
905 dependency_since.join_assign(read_hold.since());
906 }
907
908 let data_source = description.data_source;
909
910 if !dependency_read_holds.is_empty()
919 && !is_in_txns(id, &metadata)
920 && !matches!(&data_source, DataSource::Sink { .. })
921 {
922 if dependency_since.is_empty() {
928 halt!(
929 "dependency since frontier is empty while dependent upper \
930 is not empty (dependent id={id}, write_frontier={:?}, dependency_read_holds={:?}), \
931 this indicates concurrent deletion of a collection",
932 write_frontier,
933 dependency_read_holds,
934 );
935 }
936
937 if description.primary.is_none() {
955 mz_ore::soft_assert_or_log!(
956 write_frontier.elements() == &[Timestamp::MIN]
957 || write_frontier.is_empty()
958 || PartialOrder::less_than(&dependency_since, write_frontier),
959 "dependency since has advanced past dependent ({id}) upper \n
960 dependent ({id}): upper {:?} \n
961 dependency since {:?} \n
962 dependency read holds: {:?}",
963 write_frontier,
964 dependency_since,
965 dependency_read_holds,
966 );
967 }
968 }
969
970 let mut extra_state = CollectionStateExtra::None;
972 let mut maybe_instance_id = None;
973 match &data_source {
974 DataSource::Introspection(typ) => {
975 debug!(
976 ?data_source, meta = ?metadata,
977 "registering {id} with persist monotonic worker",
978 );
979 self.register_introspection_collection(
985 id,
986 *typ,
987 write,
988 persist_client.clone(),
989 )?;
990 }
991 DataSource::Webhook => {
992 debug!(
993 ?data_source, meta = ?metadata,
994 "registering {id} with persist monotonic worker",
995 );
996 new_webhook_statistic_entries.insert(id);
999 self.collection_manager
1005 .register_append_only_collection(id, write, false, None);
1006 }
1007 DataSource::IngestionExport {
1008 ingestion_id,
1009 details,
1010 data_config,
1011 } => {
1012 debug!(
1013 ?data_source, meta = ?metadata,
1014 "not registering {id} with a controller persist worker",
1015 );
1016 let ingestion_state = self
1018 .collections
1019 .get_mut(ingestion_id)
1020 .expect("known to exist");
1021
1022 let instance_id = match &mut ingestion_state.data_source {
1023 DataSource::Ingestion(ingestion_desc) => {
1024 ingestion_desc.source_exports.insert(
1025 id,
1026 SourceExport {
1027 storage_metadata: (),
1028 details: details.clone(),
1029 data_config: data_config.clone(),
1030 },
1031 );
1032
1033 ingestion_desc.instance_id
1038 }
1039 _ => unreachable!(
1040 "SourceExport must only refer to primary sources that already exist"
1041 ),
1042 };
1043
1044 to_execute.remove(&id);
1046 to_execute.insert(*ingestion_id);
1047
1048 let ingestion_state = IngestionState {
1049 read_capabilities: MutableAntichain::from(dependency_since.clone()),
1050 dependency_read_holds,
1051 derived_since: dependency_since,
1052 write_frontier: Antichain::from_elem(Timestamp::MIN),
1053 hold_policy: ReadPolicy::step_back(),
1054 instance_id,
1055 hydrated_on: BTreeSet::new(),
1056 };
1057
1058 extra_state = CollectionStateExtra::Ingestion(ingestion_state);
1059 maybe_instance_id = Some(instance_id);
1060 }
1061 DataSource::Table => {
1062 debug!(
1063 ?data_source, meta = ?metadata,
1064 "registering {id} with persist table worker",
1065 );
1066 table_registers.push((id, write));
1067 }
1068 DataSource::Progress | DataSource::Other => {
1069 debug!(
1070 ?data_source, meta = ?metadata,
1071 "not registering {id} with a controller persist worker",
1072 );
1073 }
1074 DataSource::Ingestion(ingestion_desc) => {
1075 debug!(
1076 ?data_source, meta = ?metadata,
1077 "not registering {id} with a controller persist worker",
1078 );
1079
1080 let mut dependency_since = Antichain::from_elem(Timestamp::MIN);
1081 for read_hold in dependency_read_holds.iter() {
1082 dependency_since.join_assign(read_hold.since());
1083 }
1084
1085 let ingestion_state = IngestionState {
1086 read_capabilities: MutableAntichain::from(dependency_since.clone()),
1087 dependency_read_holds,
1088 derived_since: dependency_since,
1089 write_frontier: Antichain::from_elem(Timestamp::MIN),
1090 hold_policy: ReadPolicy::step_back(),
1091 instance_id: ingestion_desc.instance_id,
1092 hydrated_on: BTreeSet::new(),
1093 };
1094
1095 extra_state = CollectionStateExtra::Ingestion(ingestion_state);
1096 maybe_instance_id = Some(ingestion_desc.instance_id);
1097 }
1098 DataSource::Sink { desc } => {
1099 let mut dependency_since = Antichain::from_elem(Timestamp::MIN);
1100 for read_hold in dependency_read_holds.iter() {
1101 dependency_since.join_assign(read_hold.since());
1102 }
1103
1104 let [self_hold, read_hold] =
1105 dependency_read_holds.try_into().expect("two holds");
1106
1107 let state = ExportState::new(
1108 desc.instance_id,
1109 read_hold,
1110 self_hold,
1111 write_frontier.clone(),
1112 ReadPolicy::step_back(),
1113 );
1114 maybe_instance_id = Some(state.cluster_id);
1115 extra_state = CollectionStateExtra::Export(state);
1116 }
1117 }
1118
1119 let wallclock_lag_metrics = self.metrics.wallclock_lag_metrics(id, maybe_instance_id);
1120 let collection_state =
1121 CollectionState::new(data_source, metadata, extra_state, wallclock_lag_metrics);
1122
1123 self.collections.insert(id, collection_state);
1124 }
1125
1126 {
1127 let mut source_statistics = self.source_statistics.lock().expect("poisoned");
1128
1129 for id in new_webhook_statistic_entries {
1132 source_statistics.webhook_statistics.entry(id).or_default();
1133 }
1134
1135 }
1139
1140 if !table_registers.is_empty() {
1142 let register_ts = register_ts
1143 .expect("caller should have provided a register_ts when creating a table");
1144
1145 if self.read_only {
1146 table_registers
1156 .retain(|(id, _write_handle)| migrated_storage_collections.contains(id));
1157
1158 self.persist_table_worker
1159 .register(register_ts, table_registers)
1160 .await
1161 .expect("table worker unexpectedly shut down");
1162 } else {
1163 self.persist_table_worker
1164 .register(register_ts, table_registers)
1165 .await
1166 .expect("table worker unexpectedly shut down");
1167 }
1168 }
1169
1170 self.append_shard_mappings(new_collections.into_iter(), Diff::ONE);
1171
1172 for id in to_execute {
1174 match &self.collection(id)?.data_source {
1175 DataSource::Ingestion(ingestion) => {
1176 if !self.read_only
1177 || (ENABLE_0DT_DEPLOYMENT_SOURCES.get(self.config.config_set())
1178 && ingestion.desc.connection.supports_read_only())
1179 {
1180 self.run_ingestion(id)?;
1181 }
1182 }
1183 DataSource::IngestionExport { .. } => unreachable!(
1184 "ingestion exports do not execute directly, but instead schedule their source to be re-executed"
1185 ),
1186 DataSource::Introspection(_)
1187 | DataSource::Webhook
1188 | DataSource::Table
1189 | DataSource::Progress
1190 | DataSource::Other => {}
1191 DataSource::Sink { .. } => {
1192 if !self.read_only {
1193 self.run_export(id)?;
1194 }
1195 }
1196 };
1197 }
1198
1199 Ok(())
1200 }
1201
1202 fn check_alter_ingestion_source_desc(
1203 &mut self,
1204 ingestion_id: GlobalId,
1205 source_desc: &SourceDesc,
1206 ) -> Result<(), StorageError> {
1207 let source_collection = self.collection(ingestion_id)?;
1208 let data_source = &source_collection.data_source;
1209 match &data_source {
1210 DataSource::Ingestion(cur_ingestion) => {
1211 cur_ingestion
1212 .desc
1213 .alter_compatible(ingestion_id, source_desc)?;
1214 }
1215 o => {
1216 tracing::info!(
1217 "{ingestion_id} inalterable because its data source is {:?} and not an ingestion",
1218 o
1219 );
1220 Err(AlterError { id: ingestion_id })?
1221 }
1222 }
1223
1224 Ok(())
1225 }
1226
1227 async fn alter_ingestion_source_desc(
1228 &mut self,
1229 ingestion_ids: BTreeMap<GlobalId, SourceDesc>,
1230 ) -> Result<(), StorageError> {
1231 let mut ingestions_to_run = BTreeSet::new();
1232
1233 for (id, new_desc) in ingestion_ids {
1234 let collection = self
1235 .collections
1236 .get_mut(&id)
1237 .ok_or_else(|| StorageError::IdentifierMissing(id))?;
1238
1239 match &mut collection.data_source {
1240 DataSource::Ingestion(ingestion) => {
1241 if ingestion.desc != new_desc {
1242 tracing::info!(
1243 from = ?ingestion.desc,
1244 to = ?new_desc,
1245 "alter_ingestion_source_desc, updating"
1246 );
1247 ingestion.desc = new_desc;
1248 ingestions_to_run.insert(id);
1249 }
1250 }
1251 o => {
1252 tracing::warn!("alter_ingestion_source_desc called on {:?}", o);
1253 Err(StorageError::IdentifierInvalid(id))?;
1254 }
1255 }
1256 }
1257
1258 for id in ingestions_to_run {
1259 self.run_ingestion(id)?;
1260 }
1261 Ok(())
1262 }
1263
1264 async fn alter_ingestion_connections(
1265 &mut self,
1266 source_connections: BTreeMap<GlobalId, GenericSourceConnection<InlinedConnection>>,
1267 ) -> Result<(), StorageError> {
1268 let mut ingestions_to_run = BTreeSet::new();
1269
1270 for (id, conn) in source_connections {
1271 let collection = self
1272 .collections
1273 .get_mut(&id)
1274 .ok_or_else(|| StorageError::IdentifierMissing(id))?;
1275
1276 match &mut collection.data_source {
1277 DataSource::Ingestion(ingestion) => {
1278 if ingestion.desc.connection != conn {
1281 tracing::info!(from = ?ingestion.desc.connection, to = ?conn, "alter_ingestion_connections, updating");
1282 ingestion.desc.connection = conn;
1283 ingestions_to_run.insert(id);
1284 } else {
1285 tracing::warn!(
1286 "update_source_connection called on {id} but the \
1287 connection was the same"
1288 );
1289 }
1290 }
1291 o => {
1292 tracing::warn!("update_source_connection called on {:?}", o);
1293 Err(StorageError::IdentifierInvalid(id))?;
1294 }
1295 }
1296 }
1297
1298 for id in ingestions_to_run {
1299 self.run_ingestion(id)?;
1300 }
1301 Ok(())
1302 }
1303
1304 async fn alter_ingestion_export_data_configs(
1305 &mut self,
1306 source_exports: BTreeMap<GlobalId, SourceExportDataConfig>,
1307 ) -> Result<(), StorageError> {
1308 let mut ingestions_to_run = BTreeSet::new();
1309
1310 for (source_export_id, new_data_config) in source_exports {
1311 let source_export_collection = self
1314 .collections
1315 .get_mut(&source_export_id)
1316 .ok_or_else(|| StorageError::IdentifierMissing(source_export_id))?;
1317 let ingestion_id = match &mut source_export_collection.data_source {
1318 DataSource::IngestionExport {
1319 ingestion_id,
1320 details: _,
1321 data_config,
1322 } => {
1323 *data_config = new_data_config.clone();
1324 *ingestion_id
1325 }
1326 o => {
1327 tracing::warn!("alter_ingestion_export_data_configs called on {:?}", o);
1328 Err(StorageError::IdentifierInvalid(source_export_id))?
1329 }
1330 };
1331 let ingestion_collection = self
1334 .collections
1335 .get_mut(&ingestion_id)
1336 .ok_or_else(|| StorageError::IdentifierMissing(ingestion_id))?;
1337
1338 match &mut ingestion_collection.data_source {
1339 DataSource::Ingestion(ingestion_desc) => {
1340 let source_export = ingestion_desc
1341 .source_exports
1342 .get_mut(&source_export_id)
1343 .ok_or_else(|| StorageError::IdentifierMissing(source_export_id))?;
1344
1345 if source_export.data_config != new_data_config {
1348 tracing::info!(?source_export_id, from = ?source_export.data_config, to = ?new_data_config, "alter_ingestion_export_data_configs, updating");
1349 source_export.data_config = new_data_config;
1350
1351 ingestions_to_run.insert(ingestion_id);
1352 } else {
1353 tracing::warn!(
1354 "alter_ingestion_export_data_configs called on \
1355 export {source_export_id} of {ingestion_id} but \
1356 the data config was the same"
1357 );
1358 }
1359 }
1360 o => {
1361 tracing::warn!("alter_ingestion_export_data_configs called on {:?}", o);
1362 Err(StorageError::IdentifierInvalid(ingestion_id))?
1363 }
1364 }
1365 }
1366
1367 for id in ingestions_to_run {
1368 self.run_ingestion(id)?;
1369 }
1370 Ok(())
1371 }
1372
1373 async fn alter_table_desc(
1374 &mut self,
1375 existing_collection: GlobalId,
1376 new_collection: GlobalId,
1377 new_desc: RelationDesc,
1378 expected_version: RelationVersion,
1379 register_ts: Timestamp,
1380 ) -> Result<(), StorageError> {
1381 let data_shard = {
1382 let Controller {
1383 collections,
1384 storage_collections,
1385 ..
1386 } = self;
1387
1388 let existing = collections
1389 .get(&existing_collection)
1390 .ok_or(StorageError::IdentifierMissing(existing_collection))?;
1391 if existing.data_source != DataSource::Table {
1392 return Err(StorageError::IdentifierInvalid(existing_collection));
1393 }
1394
1395 storage_collections
1397 .alter_table_desc(
1398 existing_collection,
1399 new_collection,
1400 new_desc.clone(),
1401 expected_version,
1402 )
1403 .await?;
1404
1405 existing.collection_metadata.data_shard.clone()
1406 };
1407
1408 let persist_client = self
1409 .persist
1410 .open(self.persist_location.clone())
1411 .await
1412 .expect("invalid persist location");
1413 let write_handle = self
1414 .open_data_handles(
1415 &existing_collection,
1416 data_shard,
1417 new_desc.clone(),
1418 &persist_client,
1419 )
1420 .await;
1421
1422 let collection_meta = CollectionMetadata {
1423 persist_location: self.persist_location.clone(),
1424 data_shard,
1425 relation_desc: new_desc.clone(),
1426 txns_shard: Some(self.txns_read.txns_id().clone()),
1428 };
1429 let wallclock_lag_metrics = self.metrics.wallclock_lag_metrics(new_collection, None);
1431 let collection_state = CollectionState::new(
1432 DataSource::Table,
1433 collection_meta,
1434 CollectionStateExtra::None,
1435 wallclock_lag_metrics,
1436 );
1437
1438 self.collections.insert(new_collection, collection_state);
1441
1442 self.persist_table_worker
1443 .register(register_ts, vec![(new_collection, write_handle)])
1444 .await
1445 .expect("table worker unexpectedly shut down");
1446
1447 self.append_shard_mappings([new_collection].into_iter(), Diff::ONE);
1448
1449 Ok(())
1450 }
1451
1452 fn export(&self, id: GlobalId) -> Result<&ExportState, StorageError> {
1453 self.collections
1454 .get(&id)
1455 .and_then(|c| match &c.extra_state {
1456 CollectionStateExtra::Export(state) => Some(state),
1457 _ => None,
1458 })
1459 .ok_or(StorageError::IdentifierMissing(id))
1460 }
1461
1462 fn export_mut(&mut self, id: GlobalId) -> Result<&mut ExportState, StorageError> {
1463 self.collections
1464 .get_mut(&id)
1465 .and_then(|c| match &mut c.extra_state {
1466 CollectionStateExtra::Export(state) => Some(state),
1467 _ => None,
1468 })
1469 .ok_or(StorageError::IdentifierMissing(id))
1470 }
1471
1472 async fn create_oneshot_ingestion(
1474 &mut self,
1475 ingestion_id: uuid::Uuid,
1476 collection_id: GlobalId,
1477 instance_id: StorageInstanceId,
1478 request: OneshotIngestionRequest,
1479 result_tx: OneshotResultCallback<ProtoBatch>,
1480 ) -> Result<(), StorageError> {
1481 let collection_meta = self
1482 .collections
1483 .get(&collection_id)
1484 .ok_or_else(|| StorageError::IdentifierMissing(collection_id))?
1485 .collection_metadata
1486 .clone();
1487 let instance = self.instances.get_mut(&instance_id).ok_or_else(|| {
1488 StorageError::Generic(anyhow::anyhow!("missing cluster {instance_id}"))
1490 })?;
1491 let oneshot_cmd = RunOneshotIngestion {
1492 ingestion_id,
1493 collection_id,
1494 collection_meta,
1495 request,
1496 };
1497
1498 if !self.read_only {
1499 instance.send(StorageCommand::RunOneshotIngestion(Box::new(oneshot_cmd)));
1500 let pending = PendingOneshotIngestion {
1501 result_tx,
1502 cluster_id: instance_id,
1503 };
1504 let novel = self
1505 .pending_oneshot_ingestions
1506 .insert(ingestion_id, pending);
1507 assert_none!(novel);
1508 Ok(())
1509 } else {
1510 Err(StorageError::ReadOnly)
1511 }
1512 }
1513
1514 fn cancel_oneshot_ingestion(&mut self, ingestion_id: uuid::Uuid) -> Result<(), StorageError> {
1515 if self.read_only {
1516 return Err(StorageError::ReadOnly);
1517 }
1518
1519 let pending = self
1520 .pending_oneshot_ingestions
1521 .remove(&ingestion_id)
1522 .ok_or_else(|| {
1523 StorageError::Generic(anyhow::anyhow!("missing oneshot ingestion {ingestion_id}"))
1525 })?;
1526
1527 match self.instances.get_mut(&pending.cluster_id) {
1528 Some(instance) => {
1529 instance.send(StorageCommand::CancelOneshotIngestion(ingestion_id));
1530 }
1531 None => {
1532 mz_ore::soft_panic_or_log!(
1533 "canceling oneshot ingestion on non-existent cluster, ingestion {:?}, instance {}",
1534 ingestion_id,
1535 pending.cluster_id,
1536 );
1537 }
1538 }
1539 pending.cancel();
1541
1542 Ok(())
1543 }
1544
1545 async fn alter_export(
1546 &mut self,
1547 id: GlobalId,
1548 new_description: ExportDescription,
1549 ) -> Result<(), StorageError> {
1550 let from_id = new_description.sink.from;
1551
1552 let desired_read_holds = vec![from_id.clone(), id.clone()];
1555 let [input_hold, self_hold] = self
1556 .storage_collections
1557 .acquire_read_holds(desired_read_holds)
1558 .expect("missing dependency")
1559 .try_into()
1560 .expect("expected number of holds");
1561 let from_storage_metadata = self.storage_collections.collection_metadata(from_id)?;
1562 let to_storage_metadata = self.storage_collections.collection_metadata(id)?;
1563
1564 let cur_export = self.export_mut(id)?;
1566 let input_readable = cur_export
1567 .write_frontier
1568 .iter()
1569 .all(|t| input_hold.since().less_than(t));
1570 if !input_readable {
1571 return Err(StorageError::ReadBeforeSince(from_id));
1572 }
1573
1574 let new_export = ExportState {
1575 read_capabilities: cur_export.read_capabilities.clone(),
1576 cluster_id: new_description.instance_id,
1577 derived_since: cur_export.derived_since.clone(),
1578 read_holds: [input_hold, self_hold],
1579 read_policy: cur_export.read_policy.clone(),
1580 write_frontier: cur_export.write_frontier.clone(),
1581 };
1582 *cur_export = new_export;
1583
1584 let with_snapshot = new_description.sink.with_snapshot
1591 && !PartialOrder::less_than(&new_description.sink.as_of, &cur_export.write_frontier);
1592
1593 let cmd = RunSinkCommand {
1594 id,
1595 description: StorageSinkDesc {
1596 from: from_id,
1597 from_desc: new_description.sink.from_desc,
1598 connection: new_description.sink.connection,
1599 envelope: new_description.sink.envelope,
1600 as_of: new_description.sink.as_of,
1601 version: new_description.sink.version,
1602 from_storage_metadata,
1603 with_snapshot,
1604 to_storage_metadata,
1605 commit_interval: new_description.sink.commit_interval,
1606 },
1607 };
1608
1609 let instance = self
1611 .instances
1612 .get_mut(&new_description.instance_id)
1613 .ok_or_else(|| StorageError::ExportInstanceMissing {
1614 storage_instance_id: new_description.instance_id,
1615 export_id: id,
1616 })?;
1617
1618 instance.send(StorageCommand::RunSink(Box::new(cmd)));
1619 Ok(())
1620 }
1621
1622 async fn alter_export_connections(
1624 &mut self,
1625 exports: BTreeMap<GlobalId, StorageSinkConnection>,
1626 ) -> Result<(), StorageError> {
1627 let mut updates_by_instance =
1628 BTreeMap::<StorageInstanceId, Vec<(RunSinkCommand, ExportDescription)>>::new();
1629
1630 for (id, connection) in exports {
1631 let (mut new_export_description, as_of): (ExportDescription, _) = {
1639 let export = &self.collections[&id];
1640 let DataSource::Sink { desc } = &export.data_source else {
1641 panic!("export exists")
1642 };
1643 let CollectionStateExtra::Export(state) = &export.extra_state else {
1644 panic!("export exists")
1645 };
1646 let export_description = desc.clone();
1647 let as_of = state.input_hold().since().clone();
1648
1649 (export_description, as_of)
1650 };
1651 let current_sink = new_export_description.sink.clone();
1652
1653 new_export_description.sink.connection = connection;
1654
1655 current_sink.alter_compatible(id, &new_export_description.sink)?;
1657
1658 let from_storage_metadata = self
1659 .storage_collections
1660 .collection_metadata(new_export_description.sink.from)?;
1661 let to_storage_metadata = self.storage_collections.collection_metadata(id)?;
1662
1663 let cmd = RunSinkCommand {
1664 id,
1665 description: StorageSinkDesc {
1666 from: new_export_description.sink.from,
1667 from_desc: new_export_description.sink.from_desc.clone(),
1668 connection: new_export_description.sink.connection.clone(),
1669 envelope: new_export_description.sink.envelope,
1670 with_snapshot: new_export_description.sink.with_snapshot,
1671 version: new_export_description.sink.version,
1672 as_of: as_of.to_owned(),
1683 from_storage_metadata,
1684 to_storage_metadata,
1685 commit_interval: new_export_description.sink.commit_interval,
1686 },
1687 };
1688
1689 let update = updates_by_instance
1690 .entry(new_export_description.instance_id)
1691 .or_default();
1692 update.push((cmd, new_export_description));
1693 }
1694
1695 for (instance_id, updates) in updates_by_instance {
1696 let mut export_updates = BTreeMap::new();
1697 let mut cmds = Vec::with_capacity(updates.len());
1698
1699 for (cmd, export_state) in updates {
1700 export_updates.insert(cmd.id, export_state);
1701 cmds.push(cmd);
1702 }
1703
1704 let instance = self.instances.get_mut(&instance_id).ok_or_else(|| {
1706 StorageError::ExportInstanceMissing {
1707 storage_instance_id: instance_id,
1708 export_id: *export_updates
1709 .keys()
1710 .next()
1711 .expect("set of exports not empty"),
1712 }
1713 })?;
1714
1715 for cmd in cmds {
1716 instance.send(StorageCommand::RunSink(Box::new(cmd)));
1717 }
1718
1719 for (id, new_export_description) in export_updates {
1721 let Some(state) = self.collections.get_mut(&id) else {
1722 panic!("export known to exist")
1723 };
1724 let DataSource::Sink { desc } = &mut state.data_source else {
1725 panic!("export known to exist")
1726 };
1727 *desc = new_export_description;
1728 }
1729 }
1730
1731 Ok(())
1732 }
1733
1734 fn drop_tables(
1749 &mut self,
1750 storage_metadata: &StorageMetadata,
1751 identifiers: Vec<GlobalId>,
1752 ts: Timestamp,
1753 ) -> Result<(), StorageError> {
1754 let (table_write_ids, data_source_ids): (Vec<_>, Vec<_>) = identifiers
1756 .into_iter()
1757 .partition(|id| match self.collections[id].data_source {
1758 DataSource::Table => true,
1759 DataSource::IngestionExport { .. } | DataSource::Webhook => false,
1760 _ => panic!("identifier is not a table: {}", id),
1761 });
1762
1763 if table_write_ids.len() > 0 {
1765 let drop_notif = self
1766 .persist_table_worker
1767 .drop_handles(table_write_ids.clone(), ts);
1768 let tx = self.pending_table_handle_drops_tx.clone();
1769 mz_ore::task::spawn(|| "table-cleanup".to_string(), async move {
1770 drop_notif.await;
1771 for identifier in table_write_ids {
1772 let _ = tx.send(identifier);
1773 }
1774 });
1775 }
1776
1777 if data_source_ids.len() > 0 {
1779 self.validate_collection_ids(data_source_ids.iter().cloned())?;
1780 self.drop_sources_unvalidated(storage_metadata, data_source_ids)?;
1781 }
1782
1783 Ok(())
1784 }
1785
1786 fn drop_sources(
1787 &mut self,
1788 storage_metadata: &StorageMetadata,
1789 identifiers: Vec<GlobalId>,
1790 ) -> Result<(), StorageError> {
1791 self.validate_collection_ids(identifiers.iter().cloned())?;
1792 self.drop_sources_unvalidated(storage_metadata, identifiers)
1793 }
1794
1795 fn drop_sources_unvalidated(
1796 &mut self,
1797 storage_metadata: &StorageMetadata,
1798 ids: Vec<GlobalId>,
1799 ) -> Result<(), StorageError> {
1800 let mut ingestions_to_execute = BTreeSet::new();
1803 let mut ingestions_to_drop = BTreeSet::new();
1804 let mut source_statistics_to_drop = Vec::new();
1805
1806 let mut collections_to_drop = Vec::new();
1810
1811 for id in ids.iter() {
1812 let collection_state = self.collections.get(id);
1813
1814 if let Some(collection_state) = collection_state {
1815 match collection_state.data_source {
1816 DataSource::Webhook => {
1817 let fut = self.collection_manager.unregister_collection(*id);
1820 mz_ore::task::spawn(|| format!("storage-webhook-cleanup-{id}"), fut);
1821
1822 collections_to_drop.push(*id);
1823 source_statistics_to_drop.push(*id);
1824 }
1825 DataSource::Ingestion(_) => {
1826 ingestions_to_drop.insert(*id);
1827 source_statistics_to_drop.push(*id);
1828 }
1829 DataSource::IngestionExport { ingestion_id, .. } => {
1830 ingestions_to_execute.insert(ingestion_id);
1837
1838 let ingestion_state = match self.collections.get_mut(&ingestion_id) {
1840 Some(ingestion_collection) => ingestion_collection,
1841 None => {
1843 tracing::error!(
1844 "primary source {ingestion_id} seemingly dropped before subsource {id}"
1845 );
1846 continue;
1847 }
1848 };
1849
1850 match &mut ingestion_state.data_source {
1851 DataSource::Ingestion(ingestion_desc) => {
1852 let removed = ingestion_desc.source_exports.remove(id);
1853 mz_ore::soft_assert_or_log!(
1854 removed.is_some(),
1855 "dropped subsource {id} already removed from source exports"
1856 );
1857 }
1858 _ => unreachable!(
1859 "SourceExport must only refer to primary sources that already exist"
1860 ),
1861 };
1862
1863 ingestions_to_drop.insert(*id);
1867 source_statistics_to_drop.push(*id);
1868 }
1869 DataSource::Progress | DataSource::Table | DataSource::Other => {
1870 collections_to_drop.push(*id);
1871 }
1872 DataSource::Introspection(_) | DataSource::Sink { .. } => {
1873 soft_panic_or_log!(
1876 "drop_sources called on a {:?} (id={id}))",
1877 collection_state.data_source,
1878 );
1879 }
1880 }
1881 }
1882 }
1883
1884 ingestions_to_execute.retain(|id| !ingestions_to_drop.contains(id));
1886 for ingestion_id in ingestions_to_execute {
1887 self.run_ingestion(ingestion_id)?;
1888 }
1889
1890 let ingestion_policies = ingestions_to_drop
1897 .iter()
1898 .map(|id| (*id, ReadPolicy::ValidFrom(Antichain::new())))
1899 .collect();
1900
1901 tracing::debug!(
1902 ?ingestion_policies,
1903 "dropping sources by setting read hold policies"
1904 );
1905 self.set_hold_policies(ingestion_policies);
1906
1907 let shards_to_update: BTreeSet<_> = ingestions_to_drop
1909 .iter()
1910 .chain(collections_to_drop.iter())
1911 .cloned()
1912 .collect();
1913 self.append_shard_mappings(shards_to_update.into_iter(), Diff::MINUS_ONE);
1914
1915 let status_now = mz_ore::now::to_datetime((self.now)());
1916 let mut status_updates = vec![];
1917 for id in ingestions_to_drop.iter() {
1918 status_updates.push(StatusUpdate::new(*id, status_now, Status::Dropped));
1919 }
1920
1921 if !self.read_only {
1922 self.append_status_introspection_updates(
1923 IntrospectionType::SourceStatusHistory,
1924 status_updates,
1925 );
1926 }
1927
1928 {
1929 let mut source_statistics = self.source_statistics.lock().expect("poisoned");
1930 for id in source_statistics_to_drop {
1931 source_statistics
1932 .source_statistics
1933 .retain(|(stats_id, _), _| stats_id != &id);
1934 source_statistics
1935 .webhook_statistics
1936 .retain(|stats_id, _| stats_id != &id);
1937 }
1938 }
1939
1940 for id in ingestions_to_drop.iter().chain(collections_to_drop.iter()) {
1942 tracing::info!(%id, "dropping collection state");
1943 let collection = self
1944 .collections
1945 .remove(id)
1946 .expect("list populated after checking that self.collections contains it");
1947
1948 let instance = match &collection.extra_state {
1949 CollectionStateExtra::Ingestion(ingestion) => Some(ingestion.instance_id),
1950 CollectionStateExtra::Export(export) => Some(export.cluster_id()),
1951 CollectionStateExtra::None => None,
1952 }
1953 .and_then(|i| self.instances.get(&i));
1954
1955 if let Some(instance) = instance {
1959 let active_replicas = instance.get_active_replicas_for_object(id);
1960 if !active_replicas.is_empty() {
1961 match &collection.data_source {
1968 DataSource::Ingestion(ingestion_desc) => {
1969 if *id != ingestion_desc.remap_collection_id {
1970 self.dropped_objects.insert(
1971 ingestion_desc.remap_collection_id,
1972 active_replicas.clone(),
1973 );
1974 }
1975 }
1976 _ => {}
1977 }
1978
1979 self.dropped_objects.insert(*id, active_replicas);
1980 }
1981 }
1982 }
1983
1984 self.storage_collections
1986 .drop_collections_unvalidated(storage_metadata, ids);
1987
1988 Ok(())
1989 }
1990
1991 fn drop_sinks(
1993 &mut self,
1994 storage_metadata: &StorageMetadata,
1995 identifiers: Vec<GlobalId>,
1996 ) -> Result<(), StorageError> {
1997 self.validate_export_ids(identifiers.iter().cloned())?;
1998 self.drop_sinks_unvalidated(storage_metadata, identifiers);
1999 Ok(())
2000 }
2001
2002 fn drop_sinks_unvalidated(
2003 &mut self,
2004 storage_metadata: &StorageMetadata,
2005 mut sinks_to_drop: Vec<GlobalId>,
2006 ) {
2007 sinks_to_drop.retain(|id| self.export(*id).is_ok());
2009
2010 let drop_policy = sinks_to_drop
2017 .iter()
2018 .map(|id| (*id, ReadPolicy::ValidFrom(Antichain::new())))
2019 .collect();
2020
2021 tracing::debug!(
2022 ?drop_policy,
2023 "dropping sources by setting read hold policies"
2024 );
2025 self.set_hold_policies(drop_policy);
2026
2027 let status_now = mz_ore::now::to_datetime((self.now)());
2034
2035 let mut status_updates = vec![];
2037 {
2038 let mut sink_statistics = self.sink_statistics.lock().expect("poisoned");
2039 for id in sinks_to_drop.iter() {
2040 status_updates.push(StatusUpdate::new(*id, status_now, Status::Dropped));
2041 sink_statistics.retain(|(stats_id, _), _| stats_id != id);
2042 }
2043 }
2044
2045 if !self.read_only {
2046 self.append_status_introspection_updates(
2047 IntrospectionType::SinkStatusHistory,
2048 status_updates,
2049 );
2050 }
2051
2052 for id in sinks_to_drop.iter() {
2054 tracing::info!(%id, "dropping export state");
2055 let collection = self
2056 .collections
2057 .remove(id)
2058 .expect("list populated after checking that self.collections contains it");
2059
2060 let instance = match &collection.extra_state {
2061 CollectionStateExtra::Ingestion(ingestion) => Some(ingestion.instance_id),
2062 CollectionStateExtra::Export(export) => Some(export.cluster_id()),
2063 CollectionStateExtra::None => None,
2064 }
2065 .and_then(|i| self.instances.get(&i));
2066
2067 if let Some(instance) = instance {
2071 let active_replicas = instance.get_active_replicas_for_object(id);
2072 if !active_replicas.is_empty() {
2073 self.dropped_objects.insert(*id, active_replicas);
2074 }
2075 }
2076 }
2077
2078 self.storage_collections
2080 .drop_collections_unvalidated(storage_metadata, sinks_to_drop);
2081 }
2082
2083 #[instrument(level = "debug")]
2084 fn append_table(
2085 &mut self,
2086 write_ts: Timestamp,
2087 advance_to: Timestamp,
2088 commands: Vec<(GlobalId, Vec<TableData>)>,
2089 ) -> Result<tokio::sync::oneshot::Receiver<Result<(), StorageError>>, StorageError> {
2090 if self.read_only {
2091 if !commands
2094 .iter()
2095 .all(|(id, _)| id.is_system() && self.migrated_storage_collections.contains(id))
2096 {
2097 return Err(StorageError::ReadOnly);
2098 }
2099 }
2100
2101 for (id, updates) in commands.iter() {
2103 if !updates.is_empty() {
2104 if !write_ts.less_than(&advance_to) {
2105 return Err(StorageError::UpdateBeyondUpper(*id));
2106 }
2107 }
2108 }
2109
2110 Ok(self
2111 .persist_table_worker
2112 .append(write_ts, advance_to, commands))
2113 }
2114
2115 fn monotonic_appender(&self, id: GlobalId) -> Result<MonotonicAppender, StorageError> {
2116 self.collection_manager.monotonic_appender(id)
2117 }
2118
2119 fn webhook_statistics(&self, id: GlobalId) -> Result<Arc<WebhookStatistics>, StorageError> {
2120 let source_statistics = self.source_statistics.lock().expect("poisoned");
2122 source_statistics
2123 .webhook_statistics
2124 .get(&id)
2125 .cloned()
2126 .ok_or(StorageError::IdentifierMissing(id))
2127 }
2128
2129 async fn ready(&mut self) {
2130 if self.maintenance_scheduled {
2131 return;
2132 }
2133
2134 if !self.pending_table_handle_drops_rx.is_empty() {
2135 return;
2136 }
2137
2138 tokio::select! {
2139 Some(m) = self.instance_response_rx.recv() => {
2140 self.stashed_responses.push(m);
2141 while let Ok(m) = self.instance_response_rx.try_recv() {
2142 self.stashed_responses.push(m);
2143 }
2144 }
2145 _ = self.maintenance_ticker.tick() => {
2146 self.maintenance_scheduled = true;
2147 },
2148 };
2149 }
2150
2151 #[instrument(level = "debug")]
2152 fn process(
2153 &mut self,
2154 storage_metadata: &StorageMetadata,
2155 ) -> Result<Option<Response>, anyhow::Error> {
2156 if self.maintenance_scheduled {
2158 self.maintain();
2159 self.maintenance_scheduled = false;
2160 }
2161
2162 for instance in self.instances.values_mut() {
2163 instance.rehydrate_failed_replicas();
2164 }
2165
2166 let mut status_updates = vec![];
2167 let mut updated_frontiers = BTreeMap::new();
2168
2169 let stashed_responses = std::mem::take(&mut self.stashed_responses);
2171 for resp in stashed_responses {
2172 match resp {
2173 (_replica_id, StorageResponse::FrontierUpper(id, upper)) => {
2174 self.update_write_frontier(id, &upper);
2175 updated_frontiers.insert(id, upper);
2176 }
2177 (replica_id, StorageResponse::DroppedId(id)) => {
2178 let replica_id = replica_id.expect("DroppedId from unknown replica");
2179 if let Some(remaining_replicas) = self.dropped_objects.get_mut(&id) {
2180 remaining_replicas.remove(&replica_id);
2181 if remaining_replicas.is_empty() {
2182 self.dropped_objects.remove(&id);
2183 }
2184 } else {
2185 soft_panic_or_log!("unexpected DroppedId for {id}");
2186 }
2187 }
2188 (replica_id, StorageResponse::StatisticsUpdates(source_stats, sink_stats)) => {
2189 {
2191 let replica_id = if let Some(replica_id) = replica_id {
2198 replica_id
2199 } else {
2200 tracing::error!(
2201 ?source_stats,
2202 "missing replica_id for source statistics update"
2203 );
2204 continue;
2205 };
2206
2207 let mut shared_stats = self.source_statistics.lock().expect("poisoned");
2208
2209 for stat in source_stats {
2210 let collection_id = stat.id.clone();
2211
2212 if self.collection(collection_id).is_err() {
2213 continue;
2216 }
2217
2218 let entry = shared_stats
2219 .source_statistics
2220 .entry((stat.id, Some(replica_id)));
2221
2222 match entry {
2223 btree_map::Entry::Vacant(vacant_entry) => {
2224 let mut stats = ControllerSourceStatistics::new(
2225 collection_id,
2226 Some(replica_id),
2227 );
2228 stats.incorporate(stat);
2229 vacant_entry.insert(stats);
2230 }
2231 btree_map::Entry::Occupied(mut occupied_entry) => {
2232 occupied_entry.get_mut().incorporate(stat);
2233 }
2234 }
2235 }
2236 }
2237
2238 {
2239 let replica_id = if let Some(replica_id) = replica_id {
2250 replica_id
2251 } else {
2252 tracing::error!(
2253 ?sink_stats,
2254 "missing replica_id for sink statistics update"
2255 );
2256 continue;
2257 };
2258
2259 let mut shared_stats = self.sink_statistics.lock().expect("poisoned");
2260
2261 for stat in sink_stats {
2262 let collection_id = stat.id.clone();
2263
2264 if self.collection(collection_id).is_err() {
2265 continue;
2268 }
2269
2270 let entry = shared_stats.entry((stat.id, Some(replica_id)));
2271
2272 match entry {
2273 btree_map::Entry::Vacant(vacant_entry) => {
2274 let mut stats =
2275 ControllerSinkStatistics::new(collection_id, replica_id);
2276 stats.incorporate(stat);
2277 vacant_entry.insert(stats);
2278 }
2279 btree_map::Entry::Occupied(mut occupied_entry) => {
2280 occupied_entry.get_mut().incorporate(stat);
2281 }
2282 }
2283 }
2284 }
2285 }
2286 (replica_id, StorageResponse::StatusUpdate(mut status_update)) => {
2287 match status_update.status {
2303 Status::Running => {
2304 let collection = self.collections.get_mut(&status_update.id);
2305 match collection {
2306 Some(collection) => {
2307 match collection.extra_state {
2308 CollectionStateExtra::Ingestion(
2309 ref mut ingestion_state,
2310 ) => {
2311 if ingestion_state.hydrated_on.is_empty() {
2312 tracing::debug!(ingestion_id = %status_update.id, "ingestion is hydrated");
2313 }
2314 ingestion_state.hydrated_on.insert(replica_id.expect(
2315 "replica id should be present for status running",
2316 ));
2317 }
2318 CollectionStateExtra::Export(_) => {
2319 }
2321 CollectionStateExtra::None => {
2322 }
2324 }
2325 }
2326 None => (), }
2329 }
2330 Status::Paused => {
2331 let collection = self.collections.get_mut(&status_update.id);
2332 match collection {
2333 Some(collection) => {
2334 match collection.extra_state {
2335 CollectionStateExtra::Ingestion(
2336 ref mut ingestion_state,
2337 ) => {
2338 tracing::debug!(ingestion_id = %status_update.id, "ingestion is now paused");
2345 ingestion_state.hydrated_on.clear();
2346 }
2347 CollectionStateExtra::Export(_) => {
2348 }
2350 CollectionStateExtra::None => {
2351 }
2353 }
2354 }
2355 None => (), }
2358 }
2359 _ => (),
2360 }
2361
2362 if let Some(id) = replica_id {
2364 status_update.replica_id = Some(id);
2365 }
2366 status_updates.push(status_update);
2367 }
2368 (_replica_id, StorageResponse::StagedBatches(batches)) => {
2369 for (ingestion_id, batches) in batches {
2370 match self.pending_oneshot_ingestions.remove(&ingestion_id) {
2371 Some(pending) => {
2372 if let Some(instance) = self.instances.get_mut(&pending.cluster_id)
2375 {
2376 instance
2377 .send(StorageCommand::CancelOneshotIngestion(ingestion_id));
2378 }
2379 (pending.result_tx)(batches)
2381 }
2382 None => {
2383 }
2386 }
2387 }
2388 }
2389 }
2390 }
2391
2392 self.record_status_updates(status_updates);
2393
2394 let mut dropped_table_ids = Vec::new();
2396 while let Ok(dropped_id) = self.pending_table_handle_drops_rx.try_recv() {
2397 dropped_table_ids.push(dropped_id);
2398 }
2399 if !dropped_table_ids.is_empty() {
2400 self.drop_sources(storage_metadata, dropped_table_ids)?;
2401 }
2402
2403 if updated_frontiers.is_empty() {
2404 Ok(None)
2405 } else {
2406 Ok(Some(Response::FrontierUpdates(
2407 updated_frontiers.into_iter().collect(),
2408 )))
2409 }
2410 }
2411
2412 async fn inspect_persist_state(
2413 &self,
2414 id: GlobalId,
2415 ) -> Result<serde_json::Value, anyhow::Error> {
2416 let collection = &self.storage_collections.collection_metadata(id)?;
2417 let client = self
2418 .persist
2419 .open(collection.persist_location.clone())
2420 .await?;
2421 let shard_state = client
2422 .inspect_shard::<Timestamp>(&collection.data_shard)
2423 .await?;
2424 let json_state = serde_json::to_value(shard_state)?;
2425 Ok(json_state)
2426 }
2427
2428 fn append_introspection_updates(
2429 &mut self,
2430 type_: IntrospectionType,
2431 updates: Vec<(Row, Diff)>,
2432 ) {
2433 let id = self.introspection_ids[&type_];
2434 let updates = updates.into_iter().map(|update| update.into()).collect();
2435 self.collection_manager.blind_write(id, updates);
2436 }
2437
2438 fn append_status_introspection_updates(
2439 &mut self,
2440 type_: IntrospectionType,
2441 updates: Vec<StatusUpdate>,
2442 ) {
2443 let id = self.introspection_ids[&type_];
2444 let updates: Vec<_> = updates.into_iter().map(|update| update.into()).collect();
2445 if !updates.is_empty() {
2446 self.collection_manager.blind_write(id, updates);
2447 }
2448 }
2449
2450 fn update_introspection_collection(&mut self, type_: IntrospectionType, op: StorageWriteOp) {
2451 let id = self.introspection_ids[&type_];
2452 self.collection_manager.differential_write(id, op);
2453 }
2454
2455 fn append_only_introspection_tx(
2456 &self,
2457 type_: IntrospectionType,
2458 ) -> mpsc::UnboundedSender<(
2459 Vec<AppendOnlyUpdate>,
2460 oneshot::Sender<Result<(), StorageError>>,
2461 )> {
2462 let id = self.introspection_ids[&type_];
2463 self.collection_manager.append_only_write_sender(id)
2464 }
2465
2466 fn differential_introspection_tx(
2467 &self,
2468 type_: IntrospectionType,
2469 ) -> mpsc::UnboundedSender<(StorageWriteOp, oneshot::Sender<Result<(), StorageError>>)> {
2470 let id = self.introspection_ids[&type_];
2471 self.collection_manager.differential_write_sender(id)
2472 }
2473
2474 async fn real_time_recent_timestamp(
2475 &self,
2476 timestamp_objects: BTreeSet<GlobalId>,
2477 timeout: Duration,
2478 ) -> Result<BoxFuture<Result<Timestamp, StorageError>>, StorageError> {
2479 use mz_storage_types::sources::GenericSourceConnection;
2480
2481 let mut rtr_futures = BTreeMap::new();
2482
2483 for id in timestamp_objects.into_iter().filter(GlobalId::is_user) {
2485 let collection = match self.collection(id) {
2486 Ok(c) => c,
2487 Err(_) => continue,
2489 };
2490
2491 let (source_conn, remap_id) = match &collection.data_source {
2492 DataSource::Ingestion(IngestionDescription {
2493 desc: SourceDesc { connection, .. },
2494 remap_collection_id,
2495 ..
2496 }) => match connection {
2497 GenericSourceConnection::Kafka(_)
2498 | GenericSourceConnection::Postgres(_)
2499 | GenericSourceConnection::MySql(_)
2500 | GenericSourceConnection::SqlServer(_) => {
2501 (connection.clone(), *remap_collection_id)
2502 }
2503
2504 GenericSourceConnection::LoadGenerator(_) => continue,
2509 },
2510 _ => {
2512 continue;
2513 }
2514 };
2515
2516 let config = self.config().clone();
2518
2519 let read_handle = self.read_handle_for_snapshot(remap_id).await?;
2527
2528 let remap_read_hold = self
2531 .storage_collections
2532 .acquire_read_holds(vec![remap_id])
2533 .map_err(|_e| StorageError::ReadBeforeSince(remap_id))?
2534 .expect_element(|| "known to be exactly one");
2535
2536 let remap_as_of = remap_read_hold
2537 .since()
2538 .to_owned()
2539 .into_option()
2540 .ok_or(StorageError::ReadBeforeSince(remap_id))?;
2541
2542 rtr_futures.insert(
2543 id,
2544 tokio::time::timeout(timeout, async move {
2545 use mz_storage_types::sources::SourceConnection as _;
2546
2547 let as_of = Antichain::from_elem(remap_as_of);
2550 let remap_subscribe = read_handle
2551 .subscribe(as_of.clone())
2552 .await
2553 .map_err(|_| StorageError::ReadBeforeSince(remap_id))?;
2554
2555 tracing::debug!(?id, type_ = source_conn.name(), upstream = ?source_conn.external_reference(), "fetching real time recency");
2556
2557 let result = rtr::real_time_recency_ts(
2558 source_conn,
2559 id,
2560 config,
2561 as_of,
2562 remap_subscribe,
2563 )
2564 .await
2565 .map_err(|e| {
2566 tracing::debug!(?id, "real time recency error: {:?}", e);
2567 e
2568 });
2569
2570 drop(remap_read_hold);
2572
2573 result
2574 }),
2575 );
2576 }
2577
2578 Ok(Box::pin(async move {
2579 let (ids, futs): (Vec<_>, Vec<_>) = rtr_futures.into_iter().unzip();
2580 ids.into_iter()
2581 .zip_eq(futures::future::join_all(futs).await)
2582 .try_fold(Timestamp::MIN, |curr, (id, per_source_res)| {
2583 let new =
2584 per_source_res.map_err(|_e: Elapsed| StorageError::RtrTimeout(id))??;
2585 Ok::<_, StorageError>(std::cmp::max(curr, new))
2586 })
2587 }))
2588 }
2589
2590 fn dump(&self) -> Result<serde_json::Value, anyhow::Error> {
2591 let Self {
2593 build_info: _,
2594 now: _,
2595 read_only,
2596 collections,
2597 dropped_objects,
2598 persist_table_worker: _,
2599 txns_read: _,
2600 txns_metrics: _,
2601 stashed_responses,
2602 pending_table_handle_drops_tx: _,
2603 pending_table_handle_drops_rx: _,
2604 pending_oneshot_ingestions,
2605 collection_manager: _,
2606 introspection_ids,
2607 introspection_tokens: _,
2608 source_statistics: _,
2609 sink_statistics: _,
2610 statistics_interval_sender: _,
2611 instances,
2612 initialized,
2613 config,
2614 persist_location,
2615 persist: _,
2616 metrics: _,
2617 recorded_frontiers,
2618 recorded_replica_frontiers,
2619 wallclock_lag: _,
2620 wallclock_lag_last_recorded,
2621 storage_collections: _,
2622 migrated_storage_collections,
2623 maintenance_ticker: _,
2624 maintenance_scheduled,
2625 instance_response_tx: _,
2626 instance_response_rx: _,
2627 persist_warm_task: _,
2628 } = self;
2629
2630 let collections: BTreeMap<_, _> = collections
2631 .iter()
2632 .map(|(id, c)| (id.to_string(), format!("{c:?}")))
2633 .collect();
2634 let dropped_objects: BTreeMap<_, _> = dropped_objects
2635 .iter()
2636 .map(|(id, rs)| (id.to_string(), format!("{rs:?}")))
2637 .collect();
2638 let stashed_responses: Vec<_> =
2639 stashed_responses.iter().map(|r| format!("{r:?}")).collect();
2640 let pending_oneshot_ingestions: BTreeMap<_, _> = pending_oneshot_ingestions
2641 .iter()
2642 .map(|(uuid, i)| (uuid.to_string(), format!("{i:?}")))
2643 .collect();
2644 let introspection_ids: BTreeMap<_, _> = introspection_ids
2645 .iter()
2646 .map(|(typ, id)| (format!("{typ:?}"), id.to_string()))
2647 .collect();
2648 let instances: BTreeMap<_, _> = instances
2649 .iter()
2650 .map(|(id, i)| (id.to_string(), format!("{i:?}")))
2651 .collect();
2652 let recorded_frontiers: BTreeMap<_, _> = recorded_frontiers
2653 .iter()
2654 .map(|(id, fs)| (id.to_string(), format!("{fs:?}")))
2655 .collect();
2656 let recorded_replica_frontiers: Vec<_> = recorded_replica_frontiers
2657 .iter()
2658 .map(|((gid, rid), f)| (gid.to_string(), rid.to_string(), format!("{f:?}")))
2659 .collect();
2660 let migrated_storage_collections: Vec<_> = migrated_storage_collections
2661 .iter()
2662 .map(|id| id.to_string())
2663 .collect();
2664
2665 Ok(serde_json::json!({
2666 "read_only": read_only,
2667 "collections": collections,
2668 "dropped_objects": dropped_objects,
2669 "stashed_responses": stashed_responses,
2670 "pending_oneshot_ingestions": pending_oneshot_ingestions,
2671 "introspection_ids": introspection_ids,
2672 "instances": instances,
2673 "initialized": initialized,
2674 "config": format!("{config:?}"),
2675 "persist_location": format!("{persist_location:?}"),
2676 "recorded_frontiers": recorded_frontiers,
2677 "recorded_replica_frontiers": recorded_replica_frontiers,
2678 "wallclock_lag_last_recorded": format!("{wallclock_lag_last_recorded:?}"),
2679 "migrated_storage_collections": migrated_storage_collections,
2680 "maintenance_scheduled": maintenance_scheduled,
2681 }))
2682 }
2683}
2684
2685pub fn prepare_initialization(txn: &mut dyn StorageTxn) -> Result<(), StorageError> {
2692 if txn.get_txn_wal_shard().is_none() {
2693 let txns_id = ShardId::new();
2694 txn.write_txn_wal_shard(txns_id)?;
2695 }
2696
2697 Ok(())
2698}
2699
2700impl Controller
2701where
2702 Self: StorageController,
2703{
2704 pub async fn new(
2712 build_info: &'static BuildInfo,
2713 persist_location: PersistLocation,
2714 persist_clients: Arc<PersistClientCache>,
2715 now: NowFn,
2716 wallclock_lag: WallclockLagFn<Timestamp>,
2717 txns_metrics: Arc<TxnMetrics>,
2718 read_only: bool,
2719 metrics_registry: &MetricsRegistry,
2720 controller_metrics: ControllerMetrics,
2721 connection_context: ConnectionContext,
2722 txn: &dyn StorageTxn,
2723 storage_collections: Arc<dyn StorageCollections + Send + Sync>,
2724 ) -> Self {
2725 let txns_client = persist_clients
2726 .open(persist_location.clone())
2727 .await
2728 .expect("location should be valid");
2729
2730 let persist_warm_task = warm_persist_state_in_background(
2731 txns_client.clone(),
2732 txn.get_collection_metadata().into_values(),
2733 );
2734 let persist_warm_task = Some(persist_warm_task.abort_on_drop());
2735
2736 let txns_id = txn
2740 .get_txn_wal_shard()
2741 .expect("must call prepare initialization before creating storage controller");
2742
2743 let persist_table_worker = if read_only {
2744 let txns_write = txns_client
2745 .open_writer(
2746 txns_id,
2747 Arc::new(TxnsCodecRow::desc()),
2748 Arc::new(UnitSchema),
2749 Diagnostics {
2750 shard_name: "txns".to_owned(),
2751 handle_purpose: "follow txns upper".to_owned(),
2752 },
2753 )
2754 .await
2755 .expect("txns schema shouldn't change");
2756 persist_handles::PersistTableWriteWorker::new_read_only_mode(txns_write)
2757 } else {
2758 let mut txns = TxnsHandle::open(
2759 Timestamp::MIN,
2760 txns_client.clone(),
2761 txns_client.dyncfgs().clone(),
2762 Arc::clone(&txns_metrics),
2763 txns_id,
2764 Opaque::encode(&PersistEpoch::default()),
2765 )
2766 .await;
2767 txns.upgrade_version().await;
2768 persist_handles::PersistTableWriteWorker::new_txns(txns)
2769 };
2770 let txns_read = TxnsRead::start::<TxnsCodecRow>(txns_client.clone(), txns_id).await;
2771
2772 let collection_manager = collection_mgmt::CollectionManager::new(read_only, now.clone());
2773
2774 let introspection_ids = BTreeMap::new();
2775 let introspection_tokens = Arc::new(Mutex::new(BTreeMap::new()));
2776
2777 let (statistics_interval_sender, _) =
2778 channel(mz_storage_types::parameters::STATISTICS_INTERVAL_DEFAULT);
2779
2780 let (pending_table_handle_drops_tx, pending_table_handle_drops_rx) =
2781 tokio::sync::mpsc::unbounded_channel();
2782
2783 let mut maintenance_ticker = tokio::time::interval(Duration::from_secs(1));
2784 maintenance_ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
2785
2786 let (instance_response_tx, instance_response_rx) = mpsc::unbounded_channel();
2787
2788 let metrics = StorageControllerMetrics::new(metrics_registry, controller_metrics);
2789
2790 let now_dt = mz_ore::now::to_datetime(now());
2791
2792 Self {
2793 build_info,
2794 collections: BTreeMap::default(),
2795 dropped_objects: Default::default(),
2796 persist_table_worker,
2797 txns_read,
2798 txns_metrics,
2799 stashed_responses: vec![],
2800 pending_table_handle_drops_tx,
2801 pending_table_handle_drops_rx,
2802 pending_oneshot_ingestions: BTreeMap::default(),
2803 collection_manager,
2804 introspection_ids,
2805 introspection_tokens,
2806 now,
2807 read_only,
2808 source_statistics: Arc::new(Mutex::new(statistics::SourceStatistics {
2809 source_statistics: BTreeMap::new(),
2810 webhook_statistics: BTreeMap::new(),
2811 })),
2812 sink_statistics: Arc::new(Mutex::new(BTreeMap::new())),
2813 statistics_interval_sender,
2814 instances: BTreeMap::new(),
2815 initialized: false,
2816 config: StorageConfiguration::new(connection_context, mz_dyncfgs::all_dyncfgs()),
2817 persist_location,
2818 persist: persist_clients,
2819 metrics,
2820 recorded_frontiers: BTreeMap::new(),
2821 recorded_replica_frontiers: BTreeMap::new(),
2822 wallclock_lag,
2823 wallclock_lag_last_recorded: now_dt,
2824 storage_collections,
2825 migrated_storage_collections: BTreeSet::new(),
2826 maintenance_ticker,
2827 maintenance_scheduled: false,
2828 instance_response_rx,
2829 instance_response_tx,
2830 persist_warm_task,
2831 }
2832 }
2833
2834 #[instrument(level = "debug")]
2842 fn set_hold_policies(&mut self, policies: Vec<(GlobalId, ReadPolicy)>) {
2843 let mut read_capability_changes = BTreeMap::default();
2844
2845 for (id, policy) in policies.into_iter() {
2846 if let Some(collection) = self.collections.get_mut(&id) {
2847 let (write_frontier, derived_since, hold_policy) = match &mut collection.extra_state
2848 {
2849 CollectionStateExtra::Ingestion(ingestion) => (
2850 ingestion.write_frontier.borrow(),
2851 &mut ingestion.derived_since,
2852 &mut ingestion.hold_policy,
2853 ),
2854 CollectionStateExtra::None => {
2855 unreachable!("set_hold_policies is only called for ingestions");
2856 }
2857 CollectionStateExtra::Export(export) => (
2858 export.write_frontier.borrow(),
2859 &mut export.derived_since,
2860 &mut export.read_policy,
2861 ),
2862 };
2863
2864 let new_derived_since = policy.frontier(write_frontier);
2865 let mut update = swap_updates(derived_since, new_derived_since);
2866 if !update.is_empty() {
2867 read_capability_changes.insert(id, update);
2868 }
2869
2870 *hold_policy = policy;
2871 }
2872 }
2873
2874 if !read_capability_changes.is_empty() {
2875 self.update_hold_capabilities(&mut read_capability_changes);
2876 }
2877 }
2878
2879 #[instrument(level = "debug", fields(updates))]
2880 fn update_write_frontier(&mut self, id: GlobalId, new_upper: &Antichain<Timestamp>) {
2881 let mut read_capability_changes = BTreeMap::default();
2882
2883 if let Some(collection) = self.collections.get_mut(&id) {
2884 let (write_frontier, derived_since, hold_policy) = match &mut collection.extra_state {
2885 CollectionStateExtra::Ingestion(ingestion) => (
2886 &mut ingestion.write_frontier,
2887 &mut ingestion.derived_since,
2888 &ingestion.hold_policy,
2889 ),
2890 CollectionStateExtra::None => {
2891 if matches!(collection.data_source, DataSource::Progress) {
2892 } else {
2894 tracing::error!(
2895 ?collection,
2896 ?new_upper,
2897 "updated write frontier for collection which is not an ingestion"
2898 );
2899 }
2900 return;
2901 }
2902 CollectionStateExtra::Export(export) => (
2903 &mut export.write_frontier,
2904 &mut export.derived_since,
2905 &export.read_policy,
2906 ),
2907 };
2908
2909 if PartialOrder::less_than(write_frontier, new_upper) {
2910 write_frontier.clone_from(new_upper);
2911 }
2912
2913 let new_derived_since = hold_policy.frontier(write_frontier.borrow());
2914 let mut update = swap_updates(derived_since, new_derived_since);
2915 if !update.is_empty() {
2916 read_capability_changes.insert(id, update);
2917 }
2918 } else if self.dropped_objects.contains_key(&id) {
2919 } else {
2922 soft_panic_or_log!("spurious upper update for {id}: {new_upper:?}");
2923 }
2924
2925 if !read_capability_changes.is_empty() {
2926 self.update_hold_capabilities(&mut read_capability_changes);
2927 }
2928 }
2929
2930 #[instrument(level = "debug", fields(updates))]
2934 fn update_hold_capabilities(
2935 &mut self,
2936 updates: &mut BTreeMap<GlobalId, ChangeBatch<Timestamp>>,
2937 ) {
2938 let mut collections_net = BTreeMap::new();
2940
2941 while let Some(key) = updates.keys().rev().next().cloned() {
2946 let mut update = updates.remove(&key).unwrap();
2947
2948 if key.is_user() {
2949 debug!(id = %key, ?update, "update_hold_capability");
2950 }
2951
2952 if let Some(collection) = self.collections.get_mut(&key) {
2953 match &mut collection.extra_state {
2954 CollectionStateExtra::Ingestion(ingestion) => {
2955 let changes = ingestion.read_capabilities.update_iter(update.drain());
2956 update.extend(changes);
2957
2958 let (changes, frontier, _cluster_id) =
2959 collections_net.entry(key).or_insert_with(|| {
2960 (
2961 <ChangeBatch<_>>::new(),
2962 Antichain::new(),
2963 ingestion.instance_id,
2964 )
2965 });
2966
2967 changes.extend(update.drain());
2968 *frontier = ingestion.read_capabilities.frontier().to_owned();
2969 }
2970 CollectionStateExtra::None => {
2971 soft_panic_or_log!(
2973 "trying to update holds for collection {collection:?} which is not \
2974 an ingestion: {update:?}"
2975 );
2976 continue;
2977 }
2978 CollectionStateExtra::Export(export) => {
2979 let changes = export.read_capabilities.update_iter(update.drain());
2980 update.extend(changes);
2981
2982 let (changes, frontier, _cluster_id) =
2983 collections_net.entry(key).or_insert_with(|| {
2984 (<ChangeBatch<_>>::new(), Antichain::new(), export.cluster_id)
2985 });
2986
2987 changes.extend(update.drain());
2988 *frontier = export.read_capabilities.frontier().to_owned();
2989 }
2990 }
2991 } else {
2992 tracing::warn!(id = ?key, ?update, "update_hold_capabilities for unknown object");
2994 }
2995 }
2996
2997 for (key, (mut changes, frontier, cluster_id)) in collections_net {
3000 if !changes.is_empty() {
3001 if key.is_user() {
3002 debug!(id = %key, ?frontier, "downgrading ingestion read holds!");
3003 }
3004
3005 let collection = self
3006 .collections
3007 .get_mut(&key)
3008 .expect("missing collection state");
3009
3010 let read_holds = match &mut collection.extra_state {
3011 CollectionStateExtra::Ingestion(ingestion) => {
3012 ingestion.dependency_read_holds.as_mut_slice()
3013 }
3014 CollectionStateExtra::Export(export) => export.read_holds.as_mut_slice(),
3015 CollectionStateExtra::None => {
3016 soft_panic_or_log!(
3017 "trying to downgrade read holds for collection which is not an \
3018 ingestion: {collection:?}"
3019 );
3020 continue;
3021 }
3022 };
3023
3024 for read_hold in read_holds.iter_mut() {
3025 read_hold
3026 .try_downgrade(frontier.clone())
3027 .expect("we only advance the frontier");
3028 }
3029
3030 if let Some(instance) = self.instances.get_mut(&cluster_id) {
3032 instance.send(StorageCommand::AllowCompaction(key, frontier.clone()));
3033 } else {
3034 soft_panic_or_log!(
3035 "missing instance client for cluster {cluster_id} while we still have outstanding AllowCompaction command {frontier:?} for {key}"
3036 );
3037 }
3038 }
3039 }
3040 }
3041
3042 fn validate_collection_ids(
3044 &self,
3045 ids: impl Iterator<Item = GlobalId>,
3046 ) -> Result<(), StorageError> {
3047 for id in ids {
3048 self.storage_collections.check_exists(id)?;
3049 }
3050 Ok(())
3051 }
3052
3053 fn validate_export_ids(&self, ids: impl Iterator<Item = GlobalId>) -> Result<(), StorageError> {
3055 for id in ids {
3056 self.export(id)?;
3057 }
3058 Ok(())
3059 }
3060
3061 async fn open_data_handles(
3069 &self,
3070 id: &GlobalId,
3071 shard: ShardId,
3072 relation_desc: RelationDesc,
3073 persist_client: &PersistClient,
3074 ) -> WriteHandle<SourceData, (), Timestamp, StorageDiff> {
3075 let diagnostics = Diagnostics {
3076 shard_name: id.to_string(),
3077 handle_purpose: format!("controller data for {}", id),
3078 };
3079
3080 let mut write = persist_client
3081 .open_writer(
3082 shard,
3083 Arc::new(relation_desc),
3084 Arc::new(UnitSchema),
3085 diagnostics.clone(),
3086 )
3087 .await
3088 .expect("invalid persist usage");
3089
3090 write.fetch_recent_upper().await;
3099
3100 write
3101 }
3102
3103 fn register_introspection_collection(
3108 &mut self,
3109 id: GlobalId,
3110 introspection_type: IntrospectionType,
3111 write_handle: WriteHandle<SourceData, (), Timestamp, StorageDiff>,
3112 persist_client: PersistClient,
3113 ) -> Result<(), StorageError> {
3114 tracing::info!(%id, ?introspection_type, "registering introspection collection");
3115
3116 let force_writable = self.read_only && self.migrated_storage_collections.contains(&id);
3120 if force_writable {
3121 assert!(id.is_system(), "unexpected non-system global id: {id:?}");
3122 info!("writing to migrated storage collection {id} in read-only mode");
3123 }
3124
3125 let prev = self.introspection_ids.insert(introspection_type, id);
3126 assert!(
3127 prev.is_none(),
3128 "cannot have multiple IDs for introspection type"
3129 );
3130
3131 let metadata = self.storage_collections.collection_metadata(id)?.clone();
3132
3133 let read_handle_fn = move || {
3134 let persist_client = persist_client.clone();
3135 let metadata = metadata.clone();
3136
3137 let fut = async move {
3138 let read_handle = persist_client
3139 .open_leased_reader::<SourceData, (), Timestamp, StorageDiff>(
3140 metadata.data_shard,
3141 Arc::new(metadata.relation_desc.clone()),
3142 Arc::new(UnitSchema),
3143 Diagnostics {
3144 shard_name: id.to_string(),
3145 handle_purpose: format!("snapshot {}", id),
3146 },
3147 USE_CRITICAL_SINCE_SNAPSHOT.get(persist_client.dyncfgs()),
3148 )
3149 .await
3150 .expect("invalid persist usage");
3151 read_handle
3152 };
3153
3154 fut.boxed()
3155 };
3156
3157 let recent_upper = write_handle.shared_upper();
3158
3159 match CollectionManagerKind::from(&introspection_type) {
3160 CollectionManagerKind::Differential => {
3165 let statistics_retention_duration =
3166 dyncfgs::STATISTICS_RETENTION_DURATION.get(self.config().config_set());
3167
3168 let introspection_config = DifferentialIntrospectionConfig {
3170 recent_upper,
3171 introspection_type,
3172 storage_collections: Arc::clone(&self.storage_collections),
3173 collection_manager: self.collection_manager.clone(),
3174 source_statistics: Arc::clone(&self.source_statistics),
3175 sink_statistics: Arc::clone(&self.sink_statistics),
3176 statistics_interval: self.config.parameters.statistics_interval.clone(),
3177 statistics_interval_receiver: self.statistics_interval_sender.subscribe(),
3178 statistics_retention_duration,
3179 metrics: self.metrics.clone(),
3180 introspection_tokens: Arc::clone(&self.introspection_tokens),
3181 };
3182 self.collection_manager.register_differential_collection(
3183 id,
3184 write_handle,
3185 read_handle_fn,
3186 force_writable,
3187 introspection_config,
3188 );
3189 }
3190 CollectionManagerKind::AppendOnly => {
3198 let introspection_config = AppendOnlyIntrospectionConfig {
3199 introspection_type,
3200 config_set: Arc::clone(self.config.config_set()),
3201 parameters: self.config.parameters.clone(),
3202 storage_collections: Arc::clone(&self.storage_collections),
3203 };
3204 self.collection_manager.register_append_only_collection(
3205 id,
3206 write_handle,
3207 force_writable,
3208 Some(introspection_config),
3209 );
3210 }
3211 }
3212
3213 Ok(())
3214 }
3215
3216 fn reconcile_dangling_statistics(&self) {
3219 self.source_statistics
3220 .lock()
3221 .expect("poisoned")
3222 .source_statistics
3223 .retain(|(k, _replica_id), _| self.storage_collections.check_exists(*k).is_ok());
3225 self.sink_statistics
3226 .lock()
3227 .expect("poisoned")
3228 .retain(|(k, _replica_id), _| self.export(*k).is_ok());
3229 }
3230
3231 #[instrument(level = "debug")]
3241 fn append_shard_mappings<I>(&self, global_ids: I, diff: Diff)
3242 where
3243 I: Iterator<Item = GlobalId>,
3244 {
3245 mz_ore::soft_assert_or_log!(
3246 diff == Diff::MINUS_ONE || diff == Diff::ONE,
3247 "use 1 for insert or -1 for delete"
3248 );
3249
3250 let id = *self
3251 .introspection_ids
3252 .get(&IntrospectionType::ShardMapping)
3253 .expect("should be registered before this call");
3254
3255 let mut updates = vec![];
3256 let mut row_buf = Row::default();
3258
3259 for global_id in global_ids {
3260 let shard_id = if let Some(collection) = self.collections.get(&global_id) {
3261 collection.collection_metadata.data_shard.clone()
3262 } else {
3263 panic!("unknown global id: {}", global_id);
3264 };
3265
3266 let mut packer = row_buf.packer();
3267 packer.push(Datum::from(global_id.to_string().as_str()));
3268 packer.push(Datum::from(shard_id.to_string().as_str()));
3269 updates.push((row_buf.clone(), diff));
3270 }
3271
3272 self.collection_manager.differential_append(id, updates);
3273 }
3274
3275 fn determine_collection_dependencies(
3277 &self,
3278 self_id: GlobalId,
3279 collection_desc: &CollectionDescription,
3280 ) -> Result<Vec<GlobalId>, StorageError> {
3281 let mut dependencies = Vec::new();
3282
3283 if let Some(id) = collection_desc.primary {
3284 dependencies.push(id);
3285 }
3286
3287 match &collection_desc.data_source {
3288 DataSource::Introspection(_)
3289 | DataSource::Webhook
3290 | DataSource::Table
3291 | DataSource::Progress
3292 | DataSource::Other => (),
3293 DataSource::IngestionExport { ingestion_id, .. } => {
3294 let source_collection = self.collection(*ingestion_id)?;
3297 let ingestion_remap_collection_id = match &source_collection.data_source {
3298 DataSource::Ingestion(ingestion) => ingestion.remap_collection_id,
3299 _ => unreachable!(
3300 "SourceExport must only refer to primary sources that already exist"
3301 ),
3302 };
3303
3304 dependencies.extend([self_id, ingestion_remap_collection_id]);
3310 }
3311 DataSource::Ingestion(ingestion) => {
3313 dependencies.push(self_id);
3318 if self_id != ingestion.remap_collection_id {
3319 dependencies.push(ingestion.remap_collection_id);
3320 }
3321 }
3322 DataSource::Sink { desc } => {
3323 dependencies.extend([self_id, desc.sink.from]);
3325 }
3326 };
3327
3328 Ok(dependencies)
3329 }
3330
3331 async fn read_handle_for_snapshot(
3332 &self,
3333 id: GlobalId,
3334 ) -> Result<ReadHandle<SourceData, (), Timestamp, StorageDiff>, StorageError> {
3335 let metadata = self.storage_collections.collection_metadata(id)?;
3336 read_handle_for_snapshot(&self.persist, id, &metadata).await
3337 }
3338
3339 fn record_status_updates(&mut self, updates: Vec<StatusUpdate>) {
3342 if self.read_only {
3343 return;
3344 }
3345
3346 let mut sink_status_updates = vec![];
3347 let mut source_status_updates = vec![];
3348
3349 for update in updates {
3350 let id = update.id;
3351 if self.export(id).is_ok() {
3352 sink_status_updates.push(update);
3353 } else if self.storage_collections.check_exists(id).is_ok() {
3354 source_status_updates.push(update);
3355 }
3356 }
3357
3358 self.append_status_introspection_updates(
3359 IntrospectionType::SourceStatusHistory,
3360 source_status_updates,
3361 );
3362 self.append_status_introspection_updates(
3363 IntrospectionType::SinkStatusHistory,
3364 sink_status_updates,
3365 );
3366 }
3367
3368 fn collection(&self, id: GlobalId) -> Result<&CollectionState, StorageError> {
3369 self.collections
3370 .get(&id)
3371 .ok_or(StorageError::IdentifierMissing(id))
3372 }
3373
3374 fn run_ingestion(&mut self, id: GlobalId) -> Result<(), StorageError> {
3377 tracing::info!(%id, "starting ingestion");
3378
3379 let collection = self.collection(id)?;
3380 let ingestion_description = match &collection.data_source {
3381 DataSource::Ingestion(i) => i.clone(),
3382 _ => {
3383 tracing::warn!("run_ingestion called on non-ingestion ID {}", id);
3384 Err(StorageError::IdentifierInvalid(id))?
3385 }
3386 };
3387
3388 let mut source_exports = BTreeMap::new();
3390 for (export_id, export) in ingestion_description.source_exports.clone() {
3391 let export_storage_metadata = self.collection(export_id)?.collection_metadata.clone();
3392 source_exports.insert(
3393 export_id,
3394 SourceExport {
3395 storage_metadata: export_storage_metadata,
3396 details: export.details,
3397 data_config: export.data_config,
3398 },
3399 );
3400 }
3401
3402 let remap_collection = self.collection(ingestion_description.remap_collection_id)?;
3403
3404 let description = IngestionDescription::<CollectionMetadata> {
3405 source_exports,
3406 remap_metadata: remap_collection.collection_metadata.clone(),
3407 desc: ingestion_description.desc.clone(),
3409 instance_id: ingestion_description.instance_id,
3410 remap_collection_id: ingestion_description.remap_collection_id,
3411 };
3412
3413 let storage_instance_id = description.instance_id;
3414 let instance = self
3416 .instances
3417 .get_mut(&storage_instance_id)
3418 .ok_or_else(|| StorageError::IngestionInstanceMissing {
3419 storage_instance_id,
3420 ingestion_id: id,
3421 })?;
3422
3423 let augmented_ingestion = Box::new(RunIngestionCommand { id, description });
3424 instance.send(StorageCommand::RunIngestion(augmented_ingestion));
3425
3426 Ok(())
3427 }
3428
3429 fn run_export(&mut self, id: GlobalId) -> Result<(), StorageError> {
3432 let DataSource::Sink { desc: description } = &self.collections[&id].data_source else {
3433 return Err(StorageError::IdentifierMissing(id));
3434 };
3435
3436 let from_storage_metadata = self
3437 .storage_collections
3438 .collection_metadata(description.sink.from)?;
3439 let to_storage_metadata = self.storage_collections.collection_metadata(id)?;
3440
3441 let export_state = self.storage_collections.collection_frontiers(id)?;
3445 let mut as_of = description.sink.as_of.clone();
3446 as_of.join_assign(&export_state.implied_capability);
3447 let with_snapshot = description.sink.with_snapshot
3448 && !PartialOrder::less_than(&as_of, &export_state.write_frontier);
3449
3450 info!(
3451 sink_id = %id,
3452 from_id = %description.sink.from,
3453 write_frontier = ?export_state.write_frontier,
3454 ?as_of,
3455 ?with_snapshot,
3456 "run_export"
3457 );
3458
3459 let cmd = RunSinkCommand {
3460 id,
3461 description: StorageSinkDesc {
3462 from: description.sink.from,
3463 from_desc: description.sink.from_desc.clone(),
3464 connection: description.sink.connection.clone(),
3465 envelope: description.sink.envelope,
3466 as_of,
3467 version: description.sink.version,
3468 from_storage_metadata,
3469 with_snapshot,
3470 to_storage_metadata,
3471 commit_interval: description.sink.commit_interval,
3472 },
3473 };
3474
3475 let storage_instance_id = description.instance_id.clone();
3476
3477 let instance = self
3478 .instances
3479 .get_mut(&storage_instance_id)
3480 .ok_or_else(|| StorageError::ExportInstanceMissing {
3481 storage_instance_id,
3482 export_id: id,
3483 })?;
3484
3485 instance.send(StorageCommand::RunSink(Box::new(cmd)));
3486
3487 Ok(())
3488 }
3489
3490 fn update_frontier_introspection(&mut self) {
3495 let mut global_frontiers = BTreeMap::new();
3496 let mut replica_frontiers = BTreeMap::new();
3497
3498 for collection_frontiers in self.storage_collections.active_collection_frontiers() {
3499 let id = collection_frontiers.id;
3500 let since = collection_frontiers.read_capabilities;
3501 let upper = collection_frontiers.write_frontier;
3502
3503 let instance = self
3504 .collections
3505 .get(&id)
3506 .and_then(|collection_state| match &collection_state.extra_state {
3507 CollectionStateExtra::Ingestion(ingestion) => Some(ingestion.instance_id),
3508 CollectionStateExtra::Export(export) => Some(export.cluster_id()),
3509 CollectionStateExtra::None => None,
3510 })
3511 .and_then(|i| self.instances.get(&i));
3512
3513 if let Some(instance) = instance {
3514 for replica_id in instance.replica_ids() {
3515 replica_frontiers.insert((id, replica_id), upper.clone());
3516 }
3517 }
3518
3519 global_frontiers.insert(id, (since, upper));
3520 }
3521
3522 let mut global_updates = Vec::new();
3523 let mut replica_updates = Vec::new();
3524
3525 let mut push_global_update =
3526 |id: GlobalId,
3527 (since, upper): (Antichain<Timestamp>, Antichain<Timestamp>),
3528 diff: Diff| {
3529 let read_frontier = since.into_option().map_or(Datum::Null, |t| t.into());
3530 let write_frontier = upper.into_option().map_or(Datum::Null, |t| t.into());
3531 let row = Row::pack_slice(&[
3532 Datum::String(&id.to_string()),
3533 read_frontier,
3534 write_frontier,
3535 ]);
3536 global_updates.push((row, diff));
3537 };
3538
3539 let mut push_replica_update =
3540 |(id, replica_id): (GlobalId, ReplicaId), upper: Antichain<Timestamp>, diff: Diff| {
3541 let write_frontier = upper.into_option().map_or(Datum::Null, |t| t.into());
3542 let row = Row::pack_slice(&[
3543 Datum::String(&id.to_string()),
3544 Datum::String(&replica_id.to_string()),
3545 write_frontier,
3546 ]);
3547 replica_updates.push((row, diff));
3548 };
3549
3550 let mut old_global_frontiers =
3551 std::mem::replace(&mut self.recorded_frontiers, global_frontiers);
3552 for (&id, new) in &self.recorded_frontiers {
3553 match old_global_frontiers.remove(&id) {
3554 Some(old) if &old != new => {
3555 push_global_update(id, new.clone(), Diff::ONE);
3556 push_global_update(id, old, Diff::MINUS_ONE);
3557 }
3558 Some(_) => (),
3559 None => push_global_update(id, new.clone(), Diff::ONE),
3560 }
3561 }
3562 for (id, old) in old_global_frontiers {
3563 push_global_update(id, old, Diff::MINUS_ONE);
3564 }
3565
3566 let mut old_replica_frontiers =
3567 std::mem::replace(&mut self.recorded_replica_frontiers, replica_frontiers);
3568 for (&key, new) in &self.recorded_replica_frontiers {
3569 match old_replica_frontiers.remove(&key) {
3570 Some(old) if &old != new => {
3571 push_replica_update(key, new.clone(), Diff::ONE);
3572 push_replica_update(key, old, Diff::MINUS_ONE);
3573 }
3574 Some(_) => (),
3575 None => push_replica_update(key, new.clone(), Diff::ONE),
3576 }
3577 }
3578 for (key, old) in old_replica_frontiers {
3579 push_replica_update(key, old, Diff::MINUS_ONE);
3580 }
3581
3582 let id = self.introspection_ids[&IntrospectionType::Frontiers];
3583 self.collection_manager
3584 .differential_append(id, global_updates);
3585
3586 let id = self.introspection_ids[&IntrospectionType::ReplicaFrontiers];
3587 self.collection_manager
3588 .differential_append(id, replica_updates);
3589 }
3590
3591 fn refresh_wallclock_lag(&mut self) {
3610 let now_ms = (self.now)();
3611 let histogram_period =
3612 WallclockLagHistogramPeriod::from_epoch_millis(now_ms, self.config.config_set());
3613
3614 let frontier_lag = |frontier: &Antichain<Timestamp>| match frontier.as_option() {
3615 Some(ts) => (self.wallclock_lag)(*ts),
3616 None => Duration::ZERO,
3617 };
3618
3619 for frontiers in self.storage_collections.active_collection_frontiers() {
3620 let id = frontiers.id;
3621 let Some(collection) = self.collections.get_mut(&id) else {
3622 continue;
3623 };
3624
3625 let collection_unreadable =
3626 PartialOrder::less_equal(&frontiers.write_frontier, &frontiers.read_capabilities);
3627 let lag = if collection_unreadable {
3628 WallclockLag::Undefined
3629 } else {
3630 let lag = frontier_lag(&frontiers.write_frontier);
3631 WallclockLag::Seconds(lag.as_secs())
3632 };
3633
3634 collection.wallclock_lag_max = collection.wallclock_lag_max.max(lag);
3635
3636 let secs = lag.unwrap_seconds_or(u64::MAX);
3639 collection.wallclock_lag_metrics.observe(secs);
3640
3641 if let Some(stash) = &mut collection.wallclock_lag_histogram_stash {
3642 let bucket = lag.map_seconds(|secs| secs.next_power_of_two());
3643
3644 let instance_id = match &collection.extra_state {
3645 CollectionStateExtra::Ingestion(i) => Some(i.instance_id),
3646 CollectionStateExtra::Export(e) => Some(e.cluster_id()),
3647 CollectionStateExtra::None => None,
3648 };
3649 let workload_class = instance_id
3650 .and_then(|id| self.instances.get(&id))
3651 .and_then(|i| i.workload_class.clone());
3652 let labels = match workload_class {
3653 Some(wc) => [("workload_class", wc.clone())].into(),
3654 None => BTreeMap::new(),
3655 };
3656
3657 let key = (histogram_period, bucket, labels);
3658 *stash.entry(key).or_default() += Diff::ONE;
3659 }
3660 }
3661
3662 self.maybe_record_wallclock_lag();
3664 }
3665
3666 fn maybe_record_wallclock_lag(&mut self) {
3674 if self.read_only {
3675 return;
3676 }
3677
3678 let duration_trunc = |datetime: DateTime<_>, interval| {
3679 let td = TimeDelta::from_std(interval).ok()?;
3680 datetime.duration_trunc(td).ok()
3681 };
3682
3683 let interval = WALLCLOCK_LAG_RECORDING_INTERVAL.get(self.config.config_set());
3684 let now_dt = mz_ore::now::to_datetime((self.now)());
3685 let now_trunc = duration_trunc(now_dt, interval).unwrap_or_else(|| {
3686 soft_panic_or_log!("excessive wallclock lag recording interval: {interval:?}");
3687 let default = WALLCLOCK_LAG_RECORDING_INTERVAL.default();
3688 duration_trunc(now_dt, *default).unwrap()
3689 });
3690 if now_trunc <= self.wallclock_lag_last_recorded {
3691 return;
3692 }
3693
3694 let now_ts: CheckedTimestamp<_> = now_trunc.try_into().expect("must fit");
3695
3696 let mut history_updates = Vec::new();
3697 let mut histogram_updates = Vec::new();
3698 let mut row_buf = Row::default();
3699 for frontiers in self.storage_collections.active_collection_frontiers() {
3700 let id = frontiers.id;
3701 let Some(collection) = self.collections.get_mut(&id) else {
3702 continue;
3703 };
3704
3705 let max_lag = std::mem::replace(&mut collection.wallclock_lag_max, WallclockLag::MIN);
3706 let row = Row::pack_slice(&[
3707 Datum::String(&id.to_string()),
3708 Datum::Null,
3709 max_lag.into_interval_datum(),
3710 Datum::TimestampTz(now_ts),
3711 ]);
3712 history_updates.push((row, Diff::ONE));
3713
3714 let Some(stash) = &mut collection.wallclock_lag_histogram_stash else {
3715 continue;
3716 };
3717
3718 for ((period, lag, labels), count) in std::mem::take(stash) {
3719 let mut packer = row_buf.packer();
3720 packer.extend([
3721 Datum::TimestampTz(period.start),
3722 Datum::TimestampTz(period.end),
3723 Datum::String(&id.to_string()),
3724 lag.into_uint64_datum(),
3725 ]);
3726 let labels = labels.iter().map(|(k, v)| (*k, Datum::String(v)));
3727 packer.push_dict(labels);
3728
3729 histogram_updates.push((row_buf.clone(), count));
3730 }
3731 }
3732
3733 if !history_updates.is_empty() {
3734 self.append_introspection_updates(
3735 IntrospectionType::WallclockLagHistory,
3736 history_updates,
3737 );
3738 }
3739 if !histogram_updates.is_empty() {
3740 self.append_introspection_updates(
3741 IntrospectionType::WallclockLagHistogram,
3742 histogram_updates,
3743 );
3744 }
3745
3746 self.wallclock_lag_last_recorded = now_trunc;
3747 }
3748
3749 fn maintain(&mut self) {
3754 self.update_frontier_introspection();
3755 self.refresh_wallclock_lag();
3756
3757 for instance in self.instances.values_mut() {
3759 instance.refresh_state_metrics();
3760 }
3761 }
3762}
3763
3764impl From<&IntrospectionType> for CollectionManagerKind {
3765 fn from(value: &IntrospectionType) -> Self {
3766 match value {
3767 IntrospectionType::ShardMapping
3768 | IntrospectionType::Frontiers
3769 | IntrospectionType::ReplicaFrontiers
3770 | IntrospectionType::StorageSourceStatistics
3771 | IntrospectionType::StorageSinkStatistics
3772 | IntrospectionType::ComputeDependencies
3773 | IntrospectionType::ComputeOperatorHydrationStatus
3774 | IntrospectionType::ComputeMaterializedViewRefreshes
3775 | IntrospectionType::ComputeErrorCounts
3776 | IntrospectionType::ComputeHydrationTimes
3777 | IntrospectionType::ComputeObjectArrangementSizes => {
3778 CollectionManagerKind::Differential
3779 }
3780
3781 IntrospectionType::SourceStatusHistory
3782 | IntrospectionType::SinkStatusHistory
3783 | IntrospectionType::PrivatelinkConnectionStatusHistory
3784 | IntrospectionType::ReplicaStatusHistory
3785 | IntrospectionType::ReplicaMetricsHistory
3786 | IntrospectionType::WallclockLagHistory
3787 | IntrospectionType::WallclockLagHistogram
3788 | IntrospectionType::PreparedStatementHistory
3789 | IntrospectionType::StatementExecutionHistory
3790 | IntrospectionType::SessionHistory
3791 | IntrospectionType::StatementLifecycleHistory
3792 | IntrospectionType::SqlText => CollectionManagerKind::AppendOnly,
3793 }
3794 }
3795}
3796
3797async fn snapshot_statistics(
3803 id: GlobalId,
3804 upper: Antichain<Timestamp>,
3805 storage_collections: &Arc<dyn StorageCollections + Send + Sync>,
3806) -> Vec<Row> {
3807 match upper.as_option() {
3808 Some(f) if f > &Timestamp::MIN => {
3809 let as_of = f.step_back().unwrap();
3810
3811 let snapshot = storage_collections.snapshot(id, as_of).await.unwrap();
3812 snapshot
3813 .into_iter()
3814 .map(|(row, diff)| {
3815 assert_eq!(diff, 1);
3816 row
3817 })
3818 .collect()
3819 }
3820 _ => Vec::new(),
3823 }
3824}
3825
3826async fn read_handle_for_snapshot(
3827 persist: &PersistClientCache,
3828 id: GlobalId,
3829 metadata: &CollectionMetadata,
3830) -> Result<ReadHandle<SourceData, (), Timestamp, StorageDiff>, StorageError> {
3831 let persist_client = persist
3832 .open(metadata.persist_location.clone())
3833 .await
3834 .unwrap();
3835
3836 let read_handle = persist_client
3841 .open_leased_reader::<SourceData, (), _, _>(
3842 metadata.data_shard,
3843 Arc::new(metadata.relation_desc.clone()),
3844 Arc::new(UnitSchema),
3845 Diagnostics {
3846 shard_name: id.to_string(),
3847 handle_purpose: format!("snapshot {}", id),
3848 },
3849 USE_CRITICAL_SINCE_SNAPSHOT.get(persist_client.dyncfgs()),
3850 )
3851 .await
3852 .expect("invalid persist usage");
3853 Ok(read_handle)
3854}
3855
3856#[derive(Debug)]
3858struct CollectionState {
3859 pub data_source: DataSource,
3861
3862 pub collection_metadata: CollectionMetadata,
3863
3864 pub extra_state: CollectionStateExtra,
3865
3866 wallclock_lag_max: WallclockLag,
3868 wallclock_lag_histogram_stash: Option<
3875 BTreeMap<
3876 (
3877 WallclockLagHistogramPeriod,
3878 WallclockLag,
3879 BTreeMap<&'static str, String>,
3880 ),
3881 Diff,
3882 >,
3883 >,
3884 wallclock_lag_metrics: WallclockLagMetrics,
3886}
3887
3888impl CollectionState {
3889 fn new(
3890 data_source: DataSource,
3891 collection_metadata: CollectionMetadata,
3892 extra_state: CollectionStateExtra,
3893 wallclock_lag_metrics: WallclockLagMetrics,
3894 ) -> Self {
3895 let wallclock_lag_histogram_stash = match &data_source {
3899 DataSource::Other => None,
3900 _ => Some(Default::default()),
3901 };
3902
3903 Self {
3904 data_source,
3905 collection_metadata,
3906 extra_state,
3907 wallclock_lag_max: WallclockLag::MIN,
3908 wallclock_lag_histogram_stash,
3909 wallclock_lag_metrics,
3910 }
3911 }
3912}
3913
3914#[derive(Debug)]
3916enum CollectionStateExtra {
3917 Ingestion(IngestionState),
3918 Export(ExportState),
3919 None,
3920}
3921
3922#[derive(Debug)]
3924struct IngestionState {
3925 pub read_capabilities: MutableAntichain<Timestamp>,
3927
3928 pub derived_since: Antichain<Timestamp>,
3931
3932 pub dependency_read_holds: Vec<ReadHold>,
3934
3935 pub write_frontier: Antichain<Timestamp>,
3937
3938 pub hold_policy: ReadPolicy,
3945
3946 pub instance_id: StorageInstanceId,
3948
3949 pub hydrated_on: BTreeSet<ReplicaId>,
3951}
3952
3953struct StatusHistoryDesc<K> {
3958 retention_policy: StatusHistoryRetentionPolicy,
3959 extract_key: Box<dyn Fn(&[Datum]) -> K + Send>,
3960 extract_time: Box<dyn Fn(&[Datum]) -> CheckedTimestamp<DateTime<Utc>> + Send>,
3961}
3962enum StatusHistoryRetentionPolicy {
3963 LastN(usize),
3965 TimeWindow(Duration),
3967}
3968
3969fn source_status_history_desc(
3970 params: &StorageParameters,
3971) -> StatusHistoryDesc<(GlobalId, Option<ReplicaId>)> {
3972 let desc = &MZ_SOURCE_STATUS_HISTORY_DESC;
3973 let (source_id_idx, _) = desc.get_by_name(&"source_id".into()).expect("exists");
3974 let (replica_id_idx, _) = desc.get_by_name(&"replica_id".into()).expect("exists");
3975 let (time_idx, _) = desc.get_by_name(&"occurred_at".into()).expect("exists");
3976
3977 StatusHistoryDesc {
3978 retention_policy: StatusHistoryRetentionPolicy::LastN(
3979 params.keep_n_source_status_history_entries,
3980 ),
3981 extract_key: Box::new(move |datums| {
3982 (
3983 GlobalId::from_str(datums[source_id_idx].unwrap_str()).expect("GlobalId column"),
3984 if datums[replica_id_idx].is_null() {
3985 None
3986 } else {
3987 Some(
3988 ReplicaId::from_str(datums[replica_id_idx].unwrap_str())
3989 .expect("ReplicaId column"),
3990 )
3991 },
3992 )
3993 }),
3994 extract_time: Box::new(move |datums| datums[time_idx].unwrap_timestamptz()),
3995 }
3996}
3997
3998fn sink_status_history_desc(
3999 params: &StorageParameters,
4000) -> StatusHistoryDesc<(GlobalId, Option<ReplicaId>)> {
4001 let desc = &MZ_SINK_STATUS_HISTORY_DESC;
4002 let (sink_id_idx, _) = desc.get_by_name(&"sink_id".into()).expect("exists");
4003 let (replica_id_idx, _) = desc.get_by_name(&"replica_id".into()).expect("exists");
4004 let (time_idx, _) = desc.get_by_name(&"occurred_at".into()).expect("exists");
4005
4006 StatusHistoryDesc {
4007 retention_policy: StatusHistoryRetentionPolicy::LastN(
4008 params.keep_n_sink_status_history_entries,
4009 ),
4010 extract_key: Box::new(move |datums| {
4011 (
4012 GlobalId::from_str(datums[sink_id_idx].unwrap_str()).expect("GlobalId column"),
4013 if datums[replica_id_idx].is_null() {
4014 None
4015 } else {
4016 Some(
4017 ReplicaId::from_str(datums[replica_id_idx].unwrap_str())
4018 .expect("ReplicaId column"),
4019 )
4020 },
4021 )
4022 }),
4023 extract_time: Box::new(move |datums| datums[time_idx].unwrap_timestamptz()),
4024 }
4025}
4026
4027fn privatelink_status_history_desc(params: &StorageParameters) -> StatusHistoryDesc<GlobalId> {
4028 let desc = &MZ_AWS_PRIVATELINK_CONNECTION_STATUS_HISTORY_DESC;
4029 let (key_idx, _) = desc.get_by_name(&"connection_id".into()).expect("exists");
4030 let (time_idx, _) = desc.get_by_name(&"occurred_at".into()).expect("exists");
4031
4032 StatusHistoryDesc {
4033 retention_policy: StatusHistoryRetentionPolicy::LastN(
4034 params.keep_n_privatelink_status_history_entries,
4035 ),
4036 extract_key: Box::new(move |datums| {
4037 GlobalId::from_str(datums[key_idx].unwrap_str()).expect("GlobalId column")
4038 }),
4039 extract_time: Box::new(move |datums| datums[time_idx].unwrap_timestamptz()),
4040 }
4041}
4042
4043fn replica_status_history_desc(params: &StorageParameters) -> StatusHistoryDesc<(GlobalId, u64)> {
4044 let desc = &REPLICA_STATUS_HISTORY_DESC;
4045 let (replica_idx, _) = desc.get_by_name(&"replica_id".into()).expect("exists");
4046 let (process_idx, _) = desc.get_by_name(&"process_id".into()).expect("exists");
4047 let (time_idx, _) = desc.get_by_name(&"occurred_at".into()).expect("exists");
4048
4049 StatusHistoryDesc {
4050 retention_policy: StatusHistoryRetentionPolicy::TimeWindow(
4051 params.replica_status_history_retention_window,
4052 ),
4053 extract_key: Box::new(move |datums| {
4054 (
4055 GlobalId::from_str(datums[replica_idx].unwrap_str()).expect("GlobalId column"),
4056 datums[process_idx].unwrap_uint64(),
4057 )
4058 }),
4059 extract_time: Box::new(move |datums| datums[time_idx].unwrap_timestamptz()),
4060 }
4061}
4062
4063fn swap_updates(
4065 from: &mut Antichain<Timestamp>,
4066 mut replace_with: Antichain<Timestamp>,
4067) -> ChangeBatch<Timestamp> {
4068 let mut update = ChangeBatch::new();
4069 if PartialOrder::less_equal(from, &replace_with) {
4070 update.extend(replace_with.iter().map(|time| (*time, 1)));
4071 std::mem::swap(from, &mut replace_with);
4072 update.extend(replace_with.iter().map(|time| (*time, -1)));
4073 }
4074 update
4075}