1use std::collections::{BTreeMap, BTreeSet};
13use std::fmt::Debug;
14use std::sync::{Arc, Mutex};
15use std::time::{Duration, Instant};
16
17use chrono::{DateTime, DurationRound, TimeDelta, Utc};
18use mz_build_info::BuildInfo;
19use mz_cluster_client::WallclockLagFn;
20use mz_compute_types::dataflows::{BuildDesc, DataflowDescription};
21use mz_compute_types::plan::render_plan::RenderPlan;
22use mz_compute_types::sinks::{
23 ComputeSinkConnection, ComputeSinkDesc, MaterializedViewSinkConnection,
24};
25use mz_compute_types::sources::SourceInstanceDesc;
26use mz_controller_types::dyncfgs::{
27 ENABLE_PAUSED_CLUSTER_READHOLD_DOWNGRADE, WALLCLOCK_LAG_RECORDING_INTERVAL,
28};
29use mz_dyncfg::{ConfigSet, ConfigUpdates};
30use mz_expr::RowSetFinishing;
31use mz_ore::cast::CastFrom;
32use mz_ore::channel::instrumented_unbounded_channel;
33use mz_ore::now::NowFn;
34use mz_ore::tracing::OpenTelemetryContext;
35use mz_ore::{soft_assert_or_log, soft_panic_or_log};
36use mz_persist_types::PersistLocation;
37use mz_repr::adt::timestamp::CheckedTimestamp;
38use mz_repr::refresh_schedule::RefreshSchedule;
39use mz_repr::{Datum, Diff, GlobalId, RelationDesc, Row, Timestamp};
40use mz_storage_client::controller::{IntrospectionType, WallclockLag, WallclockLagHistogramPeriod};
41use mz_storage_types::read_holds::{self, ReadHold};
42use mz_storage_types::read_policy::ReadPolicy;
43use thiserror::Error;
44use timely::PartialOrder;
45use timely::progress::frontier::MutableAntichain;
46use timely::progress::{Antichain, ChangeBatch};
47use tokio::sync::{mpsc, oneshot};
48use uuid::Uuid;
49
50use crate::controller::error::{
51 CollectionMissing, ERROR_TARGET_REPLICA_FAILED, HydrationCheckBadTarget,
52};
53use crate::controller::instance_client::PeekError;
54use crate::controller::replica::{ReplicaClient, ReplicaConfig};
55use crate::controller::{
56 ComputeControllerResponse, IntrospectionUpdates, PeekNotification, ReplicaId,
57 StorageCollections,
58};
59use crate::logging::LogVariant;
60use crate::metrics::IntCounter;
61use crate::metrics::{InstanceMetrics, ReplicaCollectionMetrics, ReplicaMetrics, UIntGauge};
62use crate::protocol::command::{
63 ComputeCommand, ComputeParameters, InstanceConfig, Peek, PeekTarget,
64};
65use crate::protocol::history::ComputeCommandHistory;
66use crate::protocol::response::{
67 ComputeResponse, CopyToResponse, FrontiersResponse, PeekError as ProtocolPeekError,
68 PeekResponse, StatusResponse, SubscribeBatch, SubscribeResponse,
69};
70
71#[derive(Error, Debug)]
72#[error("replica exists already: {0}")]
73pub(super) struct ReplicaExists(pub ReplicaId);
74
75#[derive(Error, Debug)]
76#[error("replica does not exist: {0}")]
77pub(super) struct ReplicaMissing(pub ReplicaId);
78
79#[derive(Error, Debug)]
80pub(super) enum DataflowCreationError {
81 #[error("collection does not exist: {0}")]
82 CollectionMissing(GlobalId),
83 #[error("replica does not exist: {0}")]
84 ReplicaMissing(ReplicaId),
85 #[error("dataflow definition lacks an as_of value")]
86 MissingAsOf,
87 #[error("subscribe dataflow has an empty as_of")]
88 EmptyAsOfForSubscribe,
89 #[error("copy to dataflow has an empty as_of")]
90 EmptyAsOfForCopyTo,
91 #[error("no read hold provided for dataflow import: {0}")]
92 ReadHoldMissing(GlobalId),
93 #[error("insufficient read hold provided for dataflow import: {0}")]
94 ReadHoldInsufficient(GlobalId),
95}
96
97impl From<CollectionMissing> for DataflowCreationError {
98 fn from(error: CollectionMissing) -> Self {
99 Self::CollectionMissing(error.0)
100 }
101}
102
103#[derive(Error, Debug)]
104pub(super) enum ReadPolicyError {
105 #[error("collection does not exist: {0}")]
106 CollectionMissing(GlobalId),
107 #[error("collection is write-only: {0}")]
108 WriteOnlyCollection(GlobalId),
109}
110
111impl From<CollectionMissing> for ReadPolicyError {
112 fn from(error: CollectionMissing) -> Self {
113 Self::CollectionMissing(error.0)
114 }
115}
116
117pub(super) type Command = Box<dyn FnOnce(&mut Instance) + Send>;
119
120pub(super) type ReplicaResponse = (ReplicaId, u64, ComputeResponse);
123
124pub(super) struct Instance {
126 build_info: &'static BuildInfo,
128 storage_collections: StorageCollections,
130 initialized: bool,
132 read_only: bool,
137 workload_class: Option<String>,
141 replicas: BTreeMap<ReplicaId, ReplicaState>,
143 replica_dyncfg_overrides: BTreeMap<ReplicaId, ConfigUpdates>,
150 collections: BTreeMap<GlobalId, CollectionState>,
158 log_sources: BTreeMap<LogVariant, GlobalId>,
160 peeks: BTreeMap<Uuid, PendingPeek>,
169 subscribes: BTreeMap<GlobalId, ActiveSubscribe>,
183 copy_tos: BTreeSet<GlobalId>,
191 history: ComputeCommandHistory<UIntGauge>,
193 command_rx: mpsc::UnboundedReceiver<Command>,
195 response_tx: mpsc::UnboundedSender<ComputeControllerResponse>,
197 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
199 metrics: InstanceMetrics,
201 dyncfg: Arc<ConfigSet>,
203
204 peek_stash_persist_location: PersistLocation,
206
207 now: NowFn,
209 wallclock_lag: WallclockLagFn<Timestamp>,
211 wallclock_lag_last_recorded: DateTime<Utc>,
213
214 read_hold_tx: read_holds::ChangeTx,
219 replica_tx: mz_ore::channel::InstrumentedUnboundedSender<ReplicaResponse, IntCounter>,
221 replica_rx: mz_ore::channel::InstrumentedUnboundedReceiver<ReplicaResponse, IntCounter>,
223}
224
225impl Instance {
226 fn collection(&self, id: GlobalId) -> Result<&CollectionState, CollectionMissing> {
228 self.collections.get(&id).ok_or(CollectionMissing(id))
229 }
230
231 fn collection_mut(&mut self, id: GlobalId) -> Result<&mut CollectionState, CollectionMissing> {
233 self.collections.get_mut(&id).ok_or(CollectionMissing(id))
234 }
235
236 fn expect_collection(&self, id: GlobalId) -> &CollectionState {
242 self.collections.get(&id).expect("collection must exist")
243 }
244
245 fn expect_collection_mut(&mut self, id: GlobalId) -> &mut CollectionState {
251 self.collections
252 .get_mut(&id)
253 .expect("collection must exist")
254 }
255
256 fn collections_iter(&self) -> impl Iterator<Item = (GlobalId, &CollectionState)> {
257 self.collections.iter().map(|(id, coll)| (*id, coll))
258 }
259
260 fn replicas_hosting(
267 &self,
268 id: GlobalId,
269 ) -> Result<impl Iterator<Item = &ReplicaState>, CollectionMissing> {
270 let target = self.collection(id)?.target_replica;
271 Ok(self
272 .replicas
273 .values()
274 .filter(move |r| target.map_or(true, |t| t == r.id)))
275 }
276
277 fn add_collection(
283 &mut self,
284 id: GlobalId,
285 as_of: Antichain<Timestamp>,
286 shared: SharedCollectionState,
287 storage_dependencies: BTreeMap<GlobalId, ReadHold>,
288 compute_dependencies: BTreeMap<GlobalId, ReadHold>,
289 replica_input_read_holds: Vec<ReadHold>,
290 write_only: bool,
291 storage_sink: bool,
292 initial_as_of: Option<Antichain<Timestamp>>,
293 refresh_schedule: Option<RefreshSchedule>,
294 target_replica: Option<ReplicaId>,
295 ) {
296 let dependency_ids: Vec<GlobalId> = compute_dependencies
298 .keys()
299 .chain(storage_dependencies.keys())
300 .copied()
301 .collect();
302 let introspection = CollectionIntrospection::new(
303 id,
304 self.introspection_tx.clone(),
305 as_of.clone(),
306 storage_sink,
307 initial_as_of,
308 refresh_schedule,
309 dependency_ids,
310 );
311 let mut state = CollectionState::new(
312 id,
313 as_of.clone(),
314 shared,
315 storage_dependencies,
316 compute_dependencies,
317 Arc::clone(&self.read_hold_tx),
318 introspection,
319 );
320 state.target_replica = target_replica;
321 if write_only {
323 state.read_policy = None;
324 }
325
326 if let Some(previous) = self.collections.insert(id, state) {
327 panic!("attempt to add a collection with existing ID {id} (previous={previous:?}");
328 }
329
330 for replica in self.replicas.values_mut() {
332 if target_replica.is_some_and(|id| id != replica.id) {
333 continue;
334 }
335 replica.add_collection(id, as_of.clone(), replica_input_read_holds.clone());
336 }
337 }
338
339 fn remove_collection(&mut self, id: GlobalId) {
340 for replica in self.replicas.values_mut() {
342 replica.remove_collection(id);
343 }
344
345 self.collections.remove(&id);
347 }
348
349 fn add_replica_state(
350 &mut self,
351 id: ReplicaId,
352 client: ReplicaClient,
353 config: ReplicaConfig,
354 epoch: u64,
355 ) -> Result<(), read_holds::ReadHoldIssuerHungUp> {
356 let log_ids: BTreeSet<_> = config.logging.index_logs.values().copied().collect();
357
358 let metrics = self.metrics.for_replica(id);
359 let mut replica = ReplicaState::new(
360 id,
361 client,
362 config,
363 metrics,
364 self.introspection_tx.clone(),
365 epoch,
366 );
367
368 let mut shutdown_input = None;
370 for (collection_id, collection) in &self.collections {
371 if (collection.log_collection && !log_ids.contains(collection_id))
374 || collection.target_replica.is_some_and(|rid| rid != id)
375 {
376 continue;
377 }
378
379 let as_of = if collection.log_collection {
380 Antichain::from_elem(Timestamp::MIN)
385 } else {
386 collection.read_frontier().to_owned()
387 };
388
389 let mut input_read_holds = Vec::with_capacity(collection.storage_dependencies.len());
397 let mut hung_up = Vec::new();
398 for hold in collection.storage_dependencies.values() {
399 match hold.try_clone() {
400 Ok(hold) => input_read_holds.push(hold),
401 Err(read_holds::ReadHoldIssuerHungUp(input_id)) => hung_up.push(input_id),
402 }
403 }
404 if !hung_up.is_empty() {
405 tracing::error!(
406 replica_id = %id,
407 %collection_id,
408 ?hung_up,
409 "giving up on adding replica collections: storage read hold issuers hung \
410 up, the process is shutting down",
411 );
412 shutdown_input = hung_up.into_iter().next();
413 break;
414 }
415
416 replica.add_collection(*collection_id, as_of, input_read_holds);
417 }
418
419 self.replicas.insert(id, replica);
420
421 match shutdown_input {
422 Some(input_id) => Err(read_holds::ReadHoldIssuerHungUp(input_id)),
423 None => Ok(()),
424 }
425 }
426
427 fn deliver_response(&self, response: ComputeControllerResponse) {
429 let _ = self.response_tx.send(response);
432 }
433
434 fn deliver_introspection_updates(&self, type_: IntrospectionType, updates: Vec<(Row, Diff)>) {
436 let _ = self.introspection_tx.send((type_, updates));
439 }
440
441 fn replica_exists(&self, id: ReplicaId) -> bool {
443 self.replicas.contains_key(&id)
444 }
445
446 fn peeks_targeting(&self, replica_id: ReplicaId) -> impl Iterator<Item = (Uuid, &PendingPeek)> {
448 self.peeks.iter().filter_map(move |(uuid, peek)| {
449 if peek.target_replica == Some(replica_id) {
450 Some((*uuid, peek))
451 } else {
452 None
453 }
454 })
455 }
456
457 fn subscribes_targeting(&self, replica_id: ReplicaId) -> impl Iterator<Item = GlobalId> + '_ {
459 self.subscribes.keys().copied().filter(move |id| {
460 let collection = self.expect_collection(*id);
461 collection.target_replica == Some(replica_id)
462 })
463 }
464
465 fn update_frontier_introspection(&mut self) {
474 for collection in self.collections.values_mut() {
475 collection
476 .introspection
477 .observe_frontiers(&collection.read_frontier(), &collection.write_frontier());
478 }
479
480 for replica in self.replicas.values_mut() {
481 for collection in replica.collections.values_mut() {
482 collection
483 .introspection
484 .observe_frontier(&collection.write_frontier);
485 }
486 }
487 }
488
489 fn refresh_state_metrics(&self) {
498 let unscheduled_collections_count =
499 self.collections.values().filter(|c| !c.scheduled).count();
500 let connected_replica_count = self
501 .replicas
502 .values()
503 .filter(|r| r.client.is_connected())
504 .count();
505
506 self.metrics
507 .replica_count
508 .set(u64::cast_from(self.replicas.len()));
509 self.metrics
510 .collection_count
511 .set(u64::cast_from(self.collections.len()));
512 self.metrics
513 .collection_unscheduled_count
514 .set(u64::cast_from(unscheduled_collections_count));
515 self.metrics
516 .peek_count
517 .set(u64::cast_from(self.peeks.len()));
518 self.metrics
519 .subscribe_count
520 .set(u64::cast_from(self.subscribes.len()));
521 self.metrics
522 .copy_to_count
523 .set(u64::cast_from(self.copy_tos.len()));
524 self.metrics
525 .connected_replica_count
526 .set(u64::cast_from(connected_replica_count));
527 }
528
529 fn refresh_wallclock_lag(&mut self) {
548 let frontier_lag = |frontier: &Antichain<Timestamp>| match frontier.as_option() {
549 Some(ts) => (self.wallclock_lag)(ts.clone()),
550 None => Duration::ZERO,
551 };
552
553 let now_ms = (self.now)();
554 let histogram_period = WallclockLagHistogramPeriod::from_epoch_millis(now_ms, &self.dyncfg);
555 let histogram_labels = match &self.workload_class {
556 Some(wc) => [("workload_class", wc.clone())].into(),
557 None => BTreeMap::new(),
558 };
559
560 let readable_storage_collections: BTreeSet<_> = self
563 .collections
564 .keys()
565 .filter_map(|id| {
566 let frontiers = self.storage_collections.collection_frontiers(*id).ok()?;
567 PartialOrder::less_than(&frontiers.read_capabilities, &frontiers.write_frontier)
568 .then_some(*id)
569 })
570 .collect();
571
572 for (id, collection) in &mut self.collections {
574 let write_frontier = collection.write_frontier();
575 let readable = if self.storage_collections.check_exists(*id).is_ok() {
576 readable_storage_collections.contains(id)
577 } else {
578 PartialOrder::less_than(&collection.read_frontier(), &write_frontier)
579 };
580
581 if let Some(stash) = &mut collection.wallclock_lag_histogram_stash {
582 let bucket = if readable {
583 let lag = frontier_lag(&write_frontier);
584 let lag = lag.as_secs().next_power_of_two();
585 WallclockLag::Seconds(lag)
586 } else {
587 WallclockLag::Undefined
588 };
589
590 let key = (histogram_period, bucket, histogram_labels.clone());
591 *stash.entry(key).or_default() += Diff::ONE;
592 }
593 }
594
595 for replica in self.replicas.values_mut() {
597 for (id, collection) in &mut replica.collections {
598 let readable = readable_storage_collections.contains(id) || collection.hydrated();
603
604 let lag = if readable {
605 let lag = frontier_lag(&collection.write_frontier);
606 WallclockLag::Seconds(lag.as_secs())
607 } else {
608 WallclockLag::Undefined
609 };
610
611 if let Some(wallclock_lag_max) = &mut collection.wallclock_lag_max {
612 *wallclock_lag_max = (*wallclock_lag_max).max(lag);
613 }
614
615 if let Some(metrics) = &mut collection.metrics {
616 let secs = lag.unwrap_seconds_or(u64::MAX);
619 metrics.wallclock_lag.observe(secs);
620 };
621 }
622 }
623
624 self.maybe_record_wallclock_lag();
626 }
627
628 fn maybe_record_wallclock_lag(&mut self) {
636 if self.read_only {
637 return;
638 }
639
640 let duration_trunc = |datetime: DateTime<_>, interval| {
641 let td = TimeDelta::from_std(interval).ok()?;
642 datetime.duration_trunc(td).ok()
643 };
644
645 let interval = WALLCLOCK_LAG_RECORDING_INTERVAL.get(&self.dyncfg);
646 let now_dt = mz_ore::now::to_datetime((self.now)());
647 let now_trunc = duration_trunc(now_dt, interval).unwrap_or_else(|| {
648 soft_panic_or_log!("excessive wallclock lag recording interval: {interval:?}");
649 let default = WALLCLOCK_LAG_RECORDING_INTERVAL.default();
650 duration_trunc(now_dt, *default).unwrap()
651 });
652 if now_trunc <= self.wallclock_lag_last_recorded {
653 return;
654 }
655
656 let now_ts: CheckedTimestamp<_> = now_trunc.try_into().expect("must fit");
657
658 let mut history_updates = Vec::new();
659 for (replica_id, replica) in &mut self.replicas {
660 for (collection_id, collection) in &mut replica.collections {
661 let Some(wallclock_lag_max) = &mut collection.wallclock_lag_max else {
662 continue;
663 };
664
665 let max_lag = std::mem::replace(wallclock_lag_max, WallclockLag::MIN);
666 let row = Row::pack_slice(&[
667 Datum::String(&collection_id.to_string()),
668 Datum::String(&replica_id.to_string()),
669 max_lag.into_interval_datum(),
670 Datum::TimestampTz(now_ts),
671 ]);
672 history_updates.push((row, Diff::ONE));
673 }
674 }
675 if !history_updates.is_empty() {
676 self.deliver_introspection_updates(
677 IntrospectionType::WallclockLagHistory,
678 history_updates,
679 );
680 }
681
682 let mut histogram_updates = Vec::new();
683 let mut row_buf = Row::default();
684 for (collection_id, collection) in &mut self.collections {
685 let Some(stash) = &mut collection.wallclock_lag_histogram_stash else {
686 continue;
687 };
688
689 for ((period, lag, labels), count) in std::mem::take(stash) {
690 let mut packer = row_buf.packer();
691 packer.extend([
692 Datum::TimestampTz(period.start),
693 Datum::TimestampTz(period.end),
694 Datum::String(&collection_id.to_string()),
695 lag.into_uint64_datum(),
696 ]);
697 let labels = labels.iter().map(|(k, v)| (*k, Datum::String(v)));
698 packer.push_dict(labels);
699
700 histogram_updates.push((row_buf.clone(), count));
701 }
702 }
703 if !histogram_updates.is_empty() {
704 self.deliver_introspection_updates(
705 IntrospectionType::WallclockLagHistogram,
706 histogram_updates,
707 );
708 }
709
710 self.wallclock_lag_last_recorded = now_trunc;
711 }
712
713 #[mz_ore::instrument(level = "debug")]
719 pub fn collection_hydrated(&self, collection_id: GlobalId) -> Result<bool, CollectionMissing> {
720 let mut hosting_replicas = self.replicas_hosting(collection_id)?.peekable();
721 if hosting_replicas.peek().is_none() {
722 return Ok(true);
723 }
724 for replica_state in hosting_replicas {
725 let collection_state = replica_state
726 .collections
727 .get(&collection_id)
728 .expect("hosting replica must have per-replica collection state");
729
730 if collection_state.hydrated() {
731 return Ok(true);
732 }
733 }
734
735 Ok(false)
736 }
737
738 #[mz_ore::instrument(level = "debug")]
744 pub fn collections_hydrated_on_replicas(
745 &self,
746 target_replica_ids: Option<Vec<ReplicaId>>,
747 exclude_collections: &BTreeSet<GlobalId>,
748 ) -> Result<bool, HydrationCheckBadTarget> {
749 if self.replicas.is_empty() {
750 return Ok(true);
751 }
752 let target_replicas: BTreeSet<ReplicaId> = self
753 .replicas
754 .keys()
755 .filter_map(|id| match target_replica_ids {
756 None => Some(id.clone()),
757 Some(ref ids) if ids.contains(id) => Some(id.clone()),
758 Some(_) => None,
759 })
760 .collect();
761 if let Some(targets) = target_replica_ids {
762 if target_replicas.is_empty() {
763 return Err(HydrationCheckBadTarget(targets));
764 }
765 }
766
767 let mut unhydrated = BTreeSet::new();
768 for (id, _collection) in self.collections_iter() {
769 if id.is_transient() || exclude_collections.contains(&id) {
770 continue;
771 }
772
773 let mut collection_hydrated = false;
774 for replica_state in self.replicas_hosting(id).expect("collection must exist") {
777 if !target_replicas.contains(&replica_state.id) {
778 continue;
779 }
780 let collection_state = replica_state
781 .collections
782 .get(&id)
783 .expect("hosting replica must have per-replica collection state");
784
785 if collection_state.hydrated() {
786 collection_hydrated = true;
787 break;
788 }
789 }
790
791 if !collection_hydrated {
792 unhydrated.insert(id);
796 }
797 }
798
799 if !unhydrated.is_empty() {
800 tracing::info!(
804 replicas = ?target_replicas,
805 collections = ?unhydrated,
806 "collections are not hydrated on any target replica",
807 );
808 }
809
810 Ok(unhydrated.is_empty())
811 }
812
813 fn cleanup_collections(&mut self) {
829 let to_remove: Vec<_> = self
830 .collections_iter()
831 .filter(|(id, collection)| {
832 collection.dropped
833 && collection.shared.lock_read_capabilities(|c| c.is_empty())
834 && self
835 .replicas
836 .values()
837 .all(|r| r.collection_frontiers_empty(*id))
838 })
839 .map(|(id, _collection)| id)
840 .collect();
841
842 for id in to_remove {
843 self.remove_collection(id);
844 }
845 }
846
847 #[mz_ore::instrument(level = "debug")]
851 pub fn dump(&self) -> Result<serde_json::Value, anyhow::Error> {
852 let Self {
859 build_info: _,
860 storage_collections: _,
861 peek_stash_persist_location: _,
862 initialized,
863 read_only,
864 workload_class,
865 replicas,
866 replica_dyncfg_overrides: _,
867 collections,
868 log_sources: _,
869 peeks,
870 subscribes,
871 copy_tos,
872 history: _,
873 command_rx: _,
874 response_tx: _,
875 introspection_tx: _,
876 metrics: _,
877 dyncfg: _,
878 now: _,
879 wallclock_lag: _,
880 wallclock_lag_last_recorded,
881 read_hold_tx: _,
882 replica_tx: _,
883 replica_rx: _,
884 } = self;
885
886 let replicas: BTreeMap<_, _> = replicas
887 .iter()
888 .map(|(id, replica)| Ok((id.to_string(), replica.dump()?)))
889 .collect::<Result<_, anyhow::Error>>()?;
890 let collections: BTreeMap<_, _> = collections
891 .iter()
892 .map(|(id, collection)| (id.to_string(), format!("{collection:?}")))
893 .collect();
894 let peeks: BTreeMap<_, _> = peeks
895 .iter()
896 .map(|(uuid, peek)| (uuid.to_string(), format!("{peek:?}")))
897 .collect();
898 let subscribes: BTreeMap<_, _> = subscribes
899 .iter()
900 .map(|(id, subscribe)| (id.to_string(), format!("{subscribe:?}")))
901 .collect();
902 let copy_tos: Vec<_> = copy_tos.iter().map(|id| id.to_string()).collect();
903 let wallclock_lag_last_recorded = format!("{wallclock_lag_last_recorded:?}");
904
905 Ok(serde_json::json!({
906 "initialized": initialized,
907 "read_only": read_only,
908 "workload_class": workload_class,
909 "replicas": replicas,
910 "collections": collections,
911 "peeks": peeks,
912 "subscribes": subscribes,
913 "copy_tos": copy_tos,
914 "wallclock_lag_last_recorded": wallclock_lag_last_recorded,
915 }))
916 }
917
918 pub(super) fn collection_write_frontier(
920 &self,
921 id: GlobalId,
922 ) -> Result<Antichain<Timestamp>, CollectionMissing> {
923 Ok(self.collection(id)?.write_frontier())
924 }
925}
926
927impl Instance {
928 pub(super) fn new(
929 build_info: &'static BuildInfo,
930 storage: StorageCollections,
931 peek_stash_persist_location: PersistLocation,
932 arranged_logs: Vec<(LogVariant, GlobalId, SharedCollectionState)>,
933 metrics: InstanceMetrics,
934 now: NowFn,
935 wallclock_lag: WallclockLagFn<Timestamp>,
936 dyncfg: Arc<ConfigSet>,
937 command_rx: mpsc::UnboundedReceiver<Command>,
938 response_tx: mpsc::UnboundedSender<ComputeControllerResponse>,
939 read_hold_tx: read_holds::ChangeTx,
940 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
941 read_only: bool,
942 ) -> Self {
943 let mut collections = BTreeMap::new();
944 let mut log_sources = BTreeMap::new();
945 for (log, id, shared) in arranged_logs {
946 let collection = CollectionState::new_log_collection(
947 id,
948 shared,
949 Arc::clone(&read_hold_tx),
950 introspection_tx.clone(),
951 );
952 collections.insert(id, collection);
953 log_sources.insert(log, id);
954 }
955
956 let history = ComputeCommandHistory::new(metrics.for_history());
957
958 let send_count = metrics.response_send_count.clone();
959 let recv_count = metrics.response_recv_count.clone();
960 let (replica_tx, replica_rx) = instrumented_unbounded_channel(send_count, recv_count);
961
962 let now_dt = mz_ore::now::to_datetime(now());
963
964 Self {
965 build_info,
966 storage_collections: storage,
967 peek_stash_persist_location,
968 initialized: false,
969 read_only,
970 workload_class: None,
971 replicas: Default::default(),
972 replica_dyncfg_overrides: Default::default(),
973 collections,
974 log_sources,
975 peeks: Default::default(),
976 subscribes: Default::default(),
977 copy_tos: Default::default(),
978 history,
979 command_rx,
980 response_tx,
981 introspection_tx,
982 metrics,
983 dyncfg,
984 now,
985 wallclock_lag,
986 wallclock_lag_last_recorded: now_dt,
987 read_hold_tx,
988 replica_tx,
989 replica_rx,
990 }
991 }
992
993 pub(super) async fn run(mut self) {
994 self.send(ComputeCommand::Hello {
995 nonce: Uuid::default(),
998 });
999
1000 let instance_config = InstanceConfig {
1001 peek_stash_persist_location: self.peek_stash_persist_location.clone(),
1002 logging: Default::default(),
1006 expiration_offset: Default::default(),
1007 arrangement_dictionary_compression: Default::default(),
1008 initial_config: Default::default(),
1009 };
1010
1011 self.send(ComputeCommand::CreateInstance(Box::new(instance_config)));
1012
1013 loop {
1014 tokio::select! {
1015 command = self.command_rx.recv() => match command {
1016 Some(cmd) => cmd(&mut self),
1017 None => break,
1018 },
1019 response = self.replica_rx.recv() => match response {
1020 Some(response) => self.handle_response(response),
1021 None => unreachable!("self owns a sender side of the channel"),
1022 }
1023 }
1024 }
1025 }
1026
1027 #[mz_ore::instrument(level = "debug")]
1029 pub fn update_configuration(&mut self, config_params: ComputeParameters) {
1030 if let Some(workload_class) = &config_params.workload_class {
1031 self.workload_class = workload_class.clone();
1032 }
1033
1034 let command = ComputeCommand::UpdateConfiguration(Box::new(config_params));
1035 self.send(command);
1036 }
1037
1038 #[mz_ore::instrument(level = "debug")]
1043 pub fn initialization_complete(&mut self) {
1044 if !self.initialized {
1046 self.send(ComputeCommand::InitializationComplete);
1047 self.initialized = true;
1048 }
1049 }
1050
1051 #[mz_ore::instrument(level = "debug")]
1055 pub fn allow_writes(&mut self, collection_id: GlobalId) -> Result<(), CollectionMissing> {
1056 let collection = self.collection_mut(collection_id)?;
1057
1058 if !collection.read_only {
1060 return Ok(());
1061 }
1062
1063 let as_of = collection.read_frontier();
1065
1066 if as_of.is_empty() {
1069 return Ok(());
1070 }
1071
1072 collection.read_only = false;
1073 self.send(ComputeCommand::AllowWrites(collection_id));
1074
1075 Ok(())
1076 }
1077
1078 #[mz_ore::instrument(level = "debug")]
1088 pub fn shutdown(&mut self) {
1089 let (_tx, rx) = mpsc::unbounded_channel();
1091 self.command_rx = rx;
1092
1093 let stray_replicas: Vec<_> = self.replicas.keys().collect();
1094 soft_assert_or_log!(
1095 stray_replicas.is_empty(),
1096 "dropped instance still has provisioned replicas: {stray_replicas:?}",
1097 );
1098 }
1099
1100 fn initiate_shutdown(&mut self) {
1106 let (_tx, rx) = mpsc::unbounded_channel();
1109 self.command_rx = rx;
1110 }
1111
1112 #[mz_ore::instrument(level = "debug")]
1114 fn send(&mut self, cmd: ComputeCommand) {
1115 self.history.push(cmd.clone());
1120
1121 let target_replica = self.target_replica(&cmd);
1122
1123 let overrides = &self.replica_dyncfg_overrides;
1126 let dyncfg = &self.dyncfg;
1127
1128 if let Some(rid) = target_replica {
1129 if let Some(replica) = self.replicas.get_mut(&rid) {
1130 let cmd = Self::specialize_command_for_replica(cmd, rid, overrides, dyncfg);
1131 let _ = replica.client.send(cmd);
1132 }
1133 } else {
1134 for (rid, replica) in self.replicas.iter_mut() {
1135 let cmd =
1136 Self::specialize_command_for_replica(cmd.clone(), *rid, overrides, dyncfg);
1137 let _ = replica.client.send(cmd);
1138 }
1139 }
1140 }
1141
1142 fn specialize_command_for_replica(
1150 mut cmd: ComputeCommand,
1151 replica_id: ReplicaId,
1152 overrides: &BTreeMap<ReplicaId, ConfigUpdates>,
1153 dyncfg: &ConfigSet,
1154 ) -> ComputeCommand {
1155 let over = overrides.get(&replica_id);
1156 match &mut cmd {
1157 ComputeCommand::UpdateConfiguration(params) => {
1158 if let Some(over) = over
1159 && !over.updates.is_empty()
1160 {
1161 params.dyncfg_updates.extend(over.clone());
1162 }
1163 }
1164 ComputeCommand::CreateInstance(config) => {
1165 let mut initial = ConfigUpdates::from(dyncfg);
1166 if let Some(over) = over {
1167 initial.extend(over.clone());
1168 }
1169 config.initial_config = initial;
1170 }
1171 _ => {}
1172 }
1173 cmd
1174 }
1175
1176 pub(super) fn update_replica_dyncfg_overrides(
1180 &mut self,
1181 overrides: BTreeMap<ReplicaId, ConfigUpdates>,
1182 ) {
1183 self.replica_dyncfg_overrides = overrides;
1184 }
1185
1186 fn target_replica(&self, cmd: &ComputeCommand) -> Option<ReplicaId> {
1194 match &cmd {
1195 ComputeCommand::Schedule(id)
1196 | ComputeCommand::AllowWrites(id)
1197 | ComputeCommand::AllowCompaction { id, .. } => {
1198 self.expect_collection(*id).target_replica
1199 }
1200 ComputeCommand::CreateDataflow(desc) => {
1201 let mut target_replica = None;
1202 for id in desc.export_ids() {
1203 if let Some(replica) = self.expect_collection(id).target_replica {
1204 if target_replica.is_some() {
1205 assert_eq!(target_replica, Some(replica));
1206 }
1207 target_replica = Some(replica);
1208 }
1209 }
1210 target_replica
1211 }
1212 ComputeCommand::Peek(_)
1214 | ComputeCommand::Hello { .. }
1215 | ComputeCommand::CreateInstance(_)
1216 | ComputeCommand::InitializationComplete
1217 | ComputeCommand::UpdateConfiguration(_)
1218 | ComputeCommand::CancelPeek { .. } => None,
1219 }
1220 }
1221
1222 #[mz_ore::instrument(level = "debug")]
1224 pub fn add_replica(
1225 &mut self,
1226 id: ReplicaId,
1227 mut config: ReplicaConfig,
1228 epoch: Option<u64>,
1229 ) -> Result<(), ReplicaExists> {
1230 if self.replica_exists(id) {
1231 return Err(ReplicaExists(id));
1232 }
1233
1234 config.logging.index_logs = self.log_sources.clone();
1235
1236 let epoch = epoch.unwrap_or(1);
1237 let metrics = self.metrics.for_replica(id);
1238 let client = ReplicaClient::spawn(
1239 id,
1240 self.build_info,
1241 config.clone(),
1242 epoch,
1243 metrics.clone(),
1244 Arc::clone(&self.dyncfg),
1245 self.replica_tx.clone(),
1246 );
1247
1248 self.history.reduce();
1250
1251 self.history.update_source_uppers(&self.storage_collections);
1253
1254 for command in self.history.iter() {
1256 if let Some(target_replica) = self.target_replica(command)
1258 && target_replica != id
1259 {
1260 continue;
1261 }
1262
1263 let command = Self::specialize_command_for_replica(
1266 command.clone(),
1267 id,
1268 &self.replica_dyncfg_overrides,
1269 &self.dyncfg,
1270 );
1271 if client.send(command).is_err() {
1272 tracing::warn!("Replica {:?} connection terminated during hydration", id);
1275 break;
1276 }
1277 }
1278
1279 if self.add_replica_state(id, client, config, epoch).is_err() {
1281 self.initiate_shutdown();
1287 }
1288
1289 Ok(())
1290 }
1291
1292 #[mz_ore::instrument(level = "debug")]
1294 pub fn remove_replica(&mut self, id: ReplicaId) -> Result<(), ReplicaMissing> {
1295 let replica = self.replicas.remove(&id).ok_or(ReplicaMissing(id))?;
1296
1297 self.replica_dyncfg_overrides.remove(&id);
1301
1302 for (collection_id, replica_collection) in &replica.collections {
1310 let collection = self.collections.get(collection_id);
1311 for replica_hold in &replica_collection.input_read_holds {
1312 let input_id = replica_hold.id();
1313 let global_hold = collection.and_then(|c| c.storage_dependencies.get(&input_id));
1314 let unprotected = global_hold
1315 .is_none_or(|h| PartialOrder::less_than(replica_hold.since(), h.since()));
1316 if unprotected {
1317 tracing::warn!(
1318 replica_id = %id,
1319 %collection_id,
1320 %input_id,
1321 replica_hold_since = ?replica_hold.since(),
1322 global_hold_since = ?global_hold.map(|h| h.since()),
1323 "dropping per-replica read hold without equivalent global read hold",
1324 );
1325 }
1326 }
1327 }
1328 drop(replica);
1329
1330 let to_drop: Vec<_> = self.subscribes_targeting(id).collect();
1334 for subscribe_id in to_drop {
1335 let subscribe = self.subscribes.remove(&subscribe_id).unwrap();
1336 let response = ComputeControllerResponse::SubscribeResponse(
1337 subscribe_id,
1338 SubscribeBatch {
1339 lower: subscribe.frontier.clone(),
1340 upper: subscribe.frontier,
1341 updates: Err(ERROR_TARGET_REPLICA_FAILED.into()),
1342 },
1343 );
1344 self.deliver_response(response);
1345 }
1346
1347 let mut peek_responses = Vec::new();
1352 let mut to_drop = Vec::new();
1353 for (uuid, peek) in self.peeks_targeting(id) {
1354 peek_responses.push(ComputeControllerResponse::PeekNotification(
1355 uuid,
1356 PeekNotification::Error(ERROR_TARGET_REPLICA_FAILED.into()),
1357 peek.otel_ctx.clone(),
1358 ));
1359 to_drop.push(uuid);
1360 }
1361 for response in peek_responses {
1362 self.deliver_response(response);
1363 }
1364 for uuid in to_drop {
1365 let response =
1366 PeekResponse::Error(ProtocolPeekError::unstructured(ERROR_TARGET_REPLICA_FAILED));
1367 self.finish_peek(uuid, response);
1368 }
1369
1370 self.forward_implied_capabilities();
1373
1374 Ok(())
1375 }
1376
1377 fn rehydrate_replica(&mut self, id: ReplicaId) {
1383 let config = self.replicas[&id].config.clone();
1384 let epoch = self.replicas[&id].epoch + 1;
1385
1386 self.remove_replica(id).expect("replica must exist");
1387 let result = self.add_replica(id, config, Some(epoch));
1388
1389 match result {
1390 Ok(()) => (),
1391 Err(ReplicaExists(_)) => unreachable!("replica was removed"),
1392 }
1393 }
1394
1395 fn rehydrate_failed_replicas(&mut self) {
1397 let replicas = self.replicas.iter();
1398 let failed_replicas: Vec<_> = replicas
1399 .filter_map(|(id, replica)| replica.client.is_failed().then_some(*id))
1400 .collect();
1401
1402 for replica_id in failed_replicas {
1403 self.rehydrate_replica(replica_id);
1404 }
1405 }
1406
1407 #[mz_ore::instrument(level = "debug")]
1412 pub fn create_dataflow(
1413 &mut self,
1414 dataflow: DataflowDescription<mz_compute_types::plan::LirRelationExpr, ()>,
1415 import_read_holds: Vec<ReadHold>,
1416 mut shared_collection_state: BTreeMap<GlobalId, SharedCollectionState>,
1417 target_replica: Option<ReplicaId>,
1418 ) -> Result<(), DataflowCreationError> {
1419 use DataflowCreationError::*;
1420
1421 if let Some(replica_id) = target_replica {
1425 if !self.replica_exists(replica_id) {
1426 return Err(ReplicaMissing(replica_id));
1427 }
1428 }
1429
1430 let as_of = dataflow.as_of.as_ref().ok_or(MissingAsOf)?;
1432 if as_of.is_empty() && dataflow.subscribe_ids().next().is_some() {
1433 return Err(EmptyAsOfForSubscribe);
1434 }
1435 if as_of.is_empty() && dataflow.copy_to_ids().next().is_some() {
1436 return Err(EmptyAsOfForCopyTo);
1437 }
1438
1439 let mut storage_dependencies = BTreeMap::new();
1441 let mut compute_dependencies = BTreeMap::new();
1442
1443 let mut replica_input_read_holds = Vec::new();
1448
1449 let mut import_read_holds: BTreeMap<_, _> =
1450 import_read_holds.into_iter().map(|r| (r.id(), r)).collect();
1451
1452 for &id in dataflow.source_imports.keys() {
1453 let mut read_hold = import_read_holds.remove(&id).ok_or(ReadHoldMissing(id))?;
1454 replica_input_read_holds.push(read_hold.clone());
1455
1456 read_hold
1457 .try_downgrade(as_of.clone())
1458 .map_err(|_| ReadHoldInsufficient(id))?;
1459 storage_dependencies.insert(id, read_hold);
1460 }
1461
1462 for &id in dataflow.index_imports.keys() {
1463 let mut read_hold = import_read_holds.remove(&id).ok_or(ReadHoldMissing(id))?;
1464 read_hold
1465 .try_downgrade(as_of.clone())
1466 .map_err(|_| ReadHoldInsufficient(id))?;
1467 compute_dependencies.insert(id, read_hold);
1468 }
1469
1470 if as_of.is_empty() {
1473 replica_input_read_holds = Default::default();
1474 }
1475
1476 for export_id in dataflow.export_ids() {
1478 let shared = shared_collection_state
1479 .remove(&export_id)
1480 .unwrap_or_else(|| SharedCollectionState::new(as_of.clone()));
1481 let write_only = dataflow.sink_exports.contains_key(&export_id);
1482 let storage_sink = dataflow.persist_sink_ids().any(|id| id == export_id);
1483
1484 self.add_collection(
1485 export_id,
1486 as_of.clone(),
1487 shared,
1488 storage_dependencies.clone(),
1489 compute_dependencies.clone(),
1490 replica_input_read_holds.clone(),
1491 write_only,
1492 storage_sink,
1493 dataflow.initial_storage_as_of.clone(),
1494 dataflow.refresh_schedule.clone(),
1495 target_replica,
1496 );
1497
1498 if let Ok(frontiers) = self.storage_collections.collection_frontiers(export_id) {
1501 self.maybe_update_global_write_frontier(export_id, frontiers.write_frontier);
1502 }
1503 }
1504
1505 for subscribe_id in dataflow.subscribe_ids() {
1507 self.subscribes
1508 .insert(subscribe_id, ActiveSubscribe::default());
1509 }
1510
1511 for copy_to_id in dataflow.copy_to_ids() {
1513 self.copy_tos.insert(copy_to_id);
1514 }
1515
1516 let mut source_imports = BTreeMap::new();
1519 for (id, import) in dataflow.source_imports {
1520 let frontiers = self
1521 .storage_collections
1522 .collection_frontiers(id)
1523 .expect("collection exists");
1524
1525 let collection_metadata = self
1526 .storage_collections
1527 .collection_metadata(id)
1528 .expect("we have a read hold on this collection");
1529
1530 let desc = SourceInstanceDesc {
1531 storage_metadata: collection_metadata.clone(),
1532 arguments: import.desc.arguments,
1533 typ: import.desc.typ.clone(),
1534 };
1535 source_imports.insert(
1536 id,
1537 mz_compute_types::dataflows::SourceImport {
1538 desc,
1539 monotonic: import.monotonic,
1540 with_snapshot: import.with_snapshot,
1541 upper: frontiers.write_frontier,
1542 },
1543 );
1544 }
1545
1546 let mut sink_exports = BTreeMap::new();
1547 for (id, se) in dataflow.sink_exports {
1548 let connection = match se.connection {
1549 ComputeSinkConnection::MaterializedView(conn) => {
1550 let metadata = self
1551 .storage_collections
1552 .collection_metadata(id)
1553 .map_err(|_| CollectionMissing(id))?
1554 .clone();
1555 let conn = MaterializedViewSinkConnection {
1556 value_desc: conn.value_desc,
1557 storage_metadata: metadata,
1558 };
1559 ComputeSinkConnection::MaterializedView(conn)
1560 }
1561 ComputeSinkConnection::Subscribe(conn) => ComputeSinkConnection::Subscribe(conn),
1562 ComputeSinkConnection::CopyToS3Oneshot(conn) => {
1563 ComputeSinkConnection::CopyToS3Oneshot(conn)
1564 }
1565 ComputeSinkConnection::MetricSink(conn) => ComputeSinkConnection::MetricSink(conn),
1566 };
1567 let desc = ComputeSinkDesc {
1568 from: se.from,
1569 from_desc: se.from_desc,
1570 connection,
1571 with_snapshot: se.with_snapshot,
1572 up_to: se.up_to,
1573 non_null_assertions: se.non_null_assertions,
1574 refresh_schedule: se.refresh_schedule,
1575 };
1576 sink_exports.insert(id, desc);
1577 }
1578
1579 let objects_to_build = dataflow
1581 .objects_to_build
1582 .into_iter()
1583 .map(|object| BuildDesc {
1584 id: object.id,
1585 plan: RenderPlan::try_from(object.plan).expect("valid plan"),
1586 })
1587 .collect();
1588
1589 let augmented_dataflow = DataflowDescription {
1590 source_imports,
1591 sink_exports,
1592 objects_to_build,
1593 index_imports: dataflow.index_imports,
1595 index_exports: dataflow.index_exports,
1596 as_of: dataflow.as_of.clone(),
1597 until: dataflow.until,
1598 initial_storage_as_of: dataflow.initial_storage_as_of,
1599 refresh_schedule: dataflow.refresh_schedule,
1600 debug_name: dataflow.debug_name,
1601 time_dependence: dataflow.time_dependence,
1602 };
1603
1604 if augmented_dataflow.is_transient() {
1605 tracing::debug!(
1606 name = %augmented_dataflow.debug_name,
1607 import_ids = %augmented_dataflow.display_import_ids(),
1608 export_ids = %augmented_dataflow.display_export_ids(),
1609 as_of = ?augmented_dataflow.as_of.as_ref().unwrap().elements(),
1610 until = ?augmented_dataflow.until.elements(),
1611 "creating dataflow",
1612 );
1613 } else {
1614 tracing::info!(
1615 name = %augmented_dataflow.debug_name,
1616 import_ids = %augmented_dataflow.display_import_ids(),
1617 export_ids = %augmented_dataflow.display_export_ids(),
1618 as_of = ?augmented_dataflow.as_of.as_ref().unwrap().elements(),
1619 until = ?augmented_dataflow.until.elements(),
1620 "creating dataflow",
1621 );
1622 }
1623
1624 if as_of.is_empty() {
1627 tracing::info!(
1628 name = %augmented_dataflow.debug_name,
1629 "not sending `CreateDataflow`, because of empty `as_of`",
1630 );
1631 } else {
1632 let collections: Vec<_> = augmented_dataflow.export_ids().collect();
1633 self.send(ComputeCommand::CreateDataflow(Box::new(augmented_dataflow)));
1634
1635 for id in collections {
1636 self.maybe_schedule_collection(id);
1637 }
1638 }
1639
1640 Ok(())
1641 }
1642
1643 fn maybe_schedule_collection(&mut self, id: GlobalId) {
1649 let collection = self.expect_collection(id);
1650
1651 if collection.scheduled {
1653 return;
1654 }
1655
1656 let as_of = collection.read_frontier();
1657
1658 if as_of.is_empty() {
1661 return;
1662 }
1663
1664 let ready = if id.is_transient() {
1665 true
1671 } else {
1672 let not_self_dep = |x: &GlobalId| *x != id;
1678
1679 let mut deps_scheduled = true;
1682
1683 let compute_deps = collection.compute_dependency_ids().filter(not_self_dep);
1688 let mut compute_frontiers = Vec::new();
1689 for id in compute_deps {
1690 let dep = &self.expect_collection(id);
1691 deps_scheduled &= dep.scheduled;
1692 compute_frontiers.push(dep.write_frontier());
1693 }
1694
1695 let storage_deps = collection.storage_dependency_ids().filter(not_self_dep);
1696 let storage_frontiers = self
1697 .storage_collections
1698 .collections_frontiers(storage_deps.collect())
1699 .expect("must exist");
1700 let storage_frontiers = storage_frontiers.into_iter().map(|f| f.write_frontier);
1701
1702 let mut frontiers = compute_frontiers.into_iter().chain(storage_frontiers);
1703 let frontiers_ready =
1704 frontiers.all(|frontier| PartialOrder::less_than(&as_of, &frontier));
1705
1706 deps_scheduled && frontiers_ready
1707 };
1708
1709 if ready {
1710 self.send(ComputeCommand::Schedule(id));
1711 let collection = self.expect_collection_mut(id);
1712 collection.scheduled = true;
1713 }
1714 }
1715
1716 fn schedule_collections(&mut self) {
1718 let ids: Vec<_> = self.collections.keys().copied().collect();
1719 for id in ids {
1720 self.maybe_schedule_collection(id);
1721 }
1722 }
1723
1724 #[mz_ore::instrument(level = "debug")]
1727 pub fn drop_collections(&mut self, ids: Vec<GlobalId>) -> Result<(), CollectionMissing> {
1728 for id in &ids {
1729 let collection = self.collection_mut(*id)?;
1730
1731 collection.dropped = true;
1733
1734 collection.implied_read_hold.release();
1737 collection.warmup_read_hold.release();
1738
1739 self.subscribes.remove(id);
1742 self.copy_tos.remove(id);
1745 }
1746
1747 Ok(())
1748 }
1749
1750 #[mz_ore::instrument(level = "debug")]
1754 pub fn peek(
1755 &mut self,
1756 peek_target: PeekTarget,
1757 literal_constraints: Option<Vec<Row>>,
1758 uuid: Uuid,
1759 timestamp: Timestamp,
1760 result_desc: RelationDesc,
1761 finishing: RowSetFinishing,
1762 map_filter_project: mz_expr::SafeMfpPlan,
1763 mut read_hold: ReadHold,
1764 target_replica: Option<ReplicaId>,
1765 peek_response_tx: oneshot::Sender<PeekResponse>,
1766 ) -> Result<(), PeekError> {
1767 use PeekError::*;
1768
1769 let target_id = peek_target.id();
1770
1771 if read_hold.id() != target_id {
1773 return Err(ReadHoldIdMismatch(read_hold.id()));
1774 }
1775 read_hold
1776 .try_downgrade(Antichain::from_elem(timestamp.clone()))
1777 .map_err(|_| ReadHoldInsufficient(target_id))?;
1778
1779 if let Some(target) = target_replica {
1780 if !self.replica_exists(target) {
1781 return Err(ReplicaMissing(target));
1782 }
1783 }
1784
1785 let otel_ctx = OpenTelemetryContext::obtain();
1786
1787 self.peeks.insert(
1788 uuid,
1789 PendingPeek {
1790 target_replica,
1791 otel_ctx: otel_ctx.clone(),
1793 requested_at: Instant::now(),
1794 read_hold,
1795 peek_response_tx,
1796 limit: finishing.limit.map(usize::cast_from),
1797 offset: finishing.offset,
1798 },
1799 );
1800
1801 let peek = Peek {
1802 literal_constraints,
1803 uuid,
1804 timestamp,
1805 finishing,
1806 map_filter_project,
1807 otel_ctx,
1810 target: peek_target,
1811 result_desc,
1812 };
1813 self.send(ComputeCommand::Peek(Box::new(peek)));
1814
1815 Ok(())
1816 }
1817
1818 #[mz_ore::instrument(level = "debug")]
1820 pub fn cancel_peek(&mut self, uuid: Uuid, reason: PeekResponse) {
1821 let Some(peek) = self.peeks.get_mut(&uuid) else {
1822 tracing::warn!("did not find pending peek for {uuid}");
1823 return;
1824 };
1825
1826 let duration = peek.requested_at.elapsed();
1827 self.metrics
1828 .observe_peek_response(&PeekResponse::Canceled, duration);
1829
1830 let otel_ctx = peek.otel_ctx.clone();
1832 otel_ctx.attach_as_parent();
1833
1834 self.deliver_response(ComputeControllerResponse::PeekNotification(
1835 uuid,
1836 PeekNotification::Canceled,
1837 otel_ctx,
1838 ));
1839
1840 self.finish_peek(uuid, reason);
1843 }
1844
1845 #[mz_ore::instrument(level = "debug")]
1857 pub fn set_read_policy(
1858 &mut self,
1859 policies: Vec<(GlobalId, ReadPolicy)>,
1860 ) -> Result<(), ReadPolicyError> {
1861 for (id, _policy) in &policies {
1864 let collection = self.collection(*id)?;
1865 if collection.read_policy.is_none() {
1866 return Err(ReadPolicyError::WriteOnlyCollection(*id));
1867 }
1868 }
1869
1870 for (id, new_policy) in policies {
1871 let collection = self.expect_collection_mut(id);
1872 let new_since = new_policy.frontier(collection.write_frontier().borrow());
1873 let _ = collection.implied_read_hold.try_downgrade(new_since);
1874 collection.read_policy = Some(new_policy);
1875 }
1876
1877 Ok(())
1878 }
1879
1880 #[mz_ore::instrument(level = "debug")]
1888 fn maybe_update_global_write_frontier(
1889 &mut self,
1890 id: GlobalId,
1891 new_frontier: Antichain<Timestamp>,
1892 ) {
1893 let collection = self.expect_collection_mut(id);
1894
1895 let advanced = collection.shared.lock_write_frontier(|f| {
1896 let advanced = PartialOrder::less_than(f, &new_frontier);
1897 if advanced {
1898 f.clone_from(&new_frontier);
1899 }
1900 advanced
1901 });
1902
1903 if !advanced {
1904 return;
1905 }
1906
1907 let new_since = match &collection.read_policy {
1909 Some(read_policy) => {
1910 read_policy.frontier(new_frontier.borrow())
1913 }
1914 None => {
1915 Antichain::from_iter(
1924 new_frontier
1925 .iter()
1926 .map(|t| t.step_back().unwrap_or(Timestamp::MIN)),
1927 )
1928 }
1929 };
1930 let _ = collection.implied_read_hold.try_downgrade(new_since);
1931
1932 self.deliver_response(ComputeControllerResponse::FrontierUpper {
1934 id,
1935 upper: new_frontier,
1936 });
1937 }
1938
1939 pub(super) fn apply_read_hold_change(
1941 &mut self,
1942 id: GlobalId,
1943 mut update: ChangeBatch<Timestamp>,
1944 ) {
1945 let Some(collection) = self.collections.get_mut(&id) else {
1946 soft_panic_or_log!(
1947 "read hold change for absent collection (id={id}, changes={update:?})"
1948 );
1949 return;
1950 };
1951
1952 let new_since = collection.shared.lock_read_capabilities(|caps| {
1953 let read_frontier = caps.frontier();
1956 for (time, diff) in update.iter() {
1957 let count = caps.count_for(time) + diff;
1958 assert!(
1959 count >= 0,
1960 "invalid read capabilities update: negative capability \
1961 (id={id:?}, read_capabilities={caps:?}, update={update:?})",
1962 );
1963 assert!(
1964 count == 0 || read_frontier.less_equal(time),
1965 "invalid read capabilities update: frontier regression \
1966 (id={id:?}, read_capabilities={caps:?}, update={update:?})",
1967 );
1968 }
1969
1970 let changes = caps.update_iter(update.drain());
1973
1974 let changed = changes.count() > 0;
1975 changed.then(|| caps.frontier().to_owned())
1976 });
1977
1978 let Some(new_since) = new_since else {
1979 return; };
1981
1982 for read_hold in collection.compute_dependencies.values_mut() {
1984 read_hold
1985 .try_downgrade(new_since.clone())
1986 .expect("frontiers don't regress");
1987 }
1988 for read_hold in collection.storage_dependencies.values_mut() {
1989 read_hold
1990 .try_downgrade(new_since.clone())
1991 .expect("frontiers don't regress");
1992 }
1993
1994 self.send(ComputeCommand::AllowCompaction {
1996 id,
1997 frontier: new_since,
1998 });
1999 }
2000
2001 fn finish_peek(&mut self, uuid: Uuid, response: PeekResponse) {
2010 let Some(peek) = self.peeks.remove(&uuid) else {
2011 return;
2012 };
2013
2014 let _ = peek.peek_response_tx.send(response);
2016
2017 self.send(ComputeCommand::CancelPeek { uuid });
2020
2021 drop(peek.read_hold);
2022 }
2023
2024 fn handle_response(&mut self, (replica_id, epoch, response): ReplicaResponse) {
2027 if self
2029 .replicas
2030 .get(&replica_id)
2031 .filter(|replica| replica.epoch == epoch)
2032 .is_none()
2033 {
2034 return;
2035 }
2036
2037 match response {
2040 ComputeResponse::Frontiers(id, frontiers) => {
2041 self.handle_frontiers_response(id, frontiers, replica_id);
2042 }
2043 ComputeResponse::PeekResponse(uuid, peek_response, otel_ctx) => {
2044 self.handle_peek_response(uuid, peek_response, otel_ctx, replica_id);
2045 }
2046 ComputeResponse::CopyToResponse(id, response) => {
2047 self.handle_copy_to_response(id, response, replica_id);
2048 }
2049 ComputeResponse::SubscribeResponse(id, response) => {
2050 self.handle_subscribe_response(id, response, replica_id);
2051 }
2052 ComputeResponse::Status(response) => {
2053 self.handle_status_response(response, replica_id);
2054 }
2055 }
2056 }
2057
2058 fn handle_frontiers_response(
2061 &mut self,
2062 id: GlobalId,
2063 frontiers: FrontiersResponse,
2064 replica_id: ReplicaId,
2065 ) {
2066 if !self.collections.contains_key(&id) {
2067 soft_panic_or_log!(
2068 "frontiers update for an unknown collection \
2069 (id={id}, replica_id={replica_id}, frontiers={frontiers:?})"
2070 );
2071 return;
2072 }
2073 let Some(replica) = self.replicas.get_mut(&replica_id) else {
2074 soft_panic_or_log!(
2075 "frontiers update for an unknown replica \
2076 (replica_id={replica_id}, frontiers={frontiers:?})"
2077 );
2078 return;
2079 };
2080 let Some(replica_collection) = replica.collections.get_mut(&id) else {
2081 soft_panic_or_log!(
2082 "frontiers update for an unknown replica collection \
2083 (id={id}, replica_id={replica_id}, frontiers={frontiers:?})"
2084 );
2085 return;
2086 };
2087
2088 if let Some(new_frontier) = frontiers.input_frontier {
2089 replica_collection.update_input_frontier(new_frontier.clone());
2090 }
2091 if let Some(new_frontier) = frontiers.output_frontier {
2092 replica_collection.update_output_frontier(new_frontier.clone());
2093 }
2094 if let Some(new_frontier) = frontiers.write_frontier {
2095 replica_collection.update_write_frontier(new_frontier.clone());
2096 self.maybe_update_global_write_frontier(id, new_frontier);
2097 }
2098 }
2099
2100 #[mz_ore::instrument(level = "debug")]
2101 fn handle_peek_response(
2102 &mut self,
2103 uuid: Uuid,
2104 response: PeekResponse,
2105 otel_ctx: OpenTelemetryContext,
2106 replica_id: ReplicaId,
2107 ) {
2108 otel_ctx.attach_as_parent();
2109
2110 let Some(peek) = self.peeks.get(&uuid) else {
2113 return;
2114 };
2115
2116 let target_replica = peek.target_replica.unwrap_or(replica_id);
2118 if target_replica != replica_id {
2119 return;
2120 }
2121
2122 let duration = peek.requested_at.elapsed();
2123 self.metrics.observe_peek_response(&response, duration);
2124
2125 let notification = PeekNotification::new(&response, peek.offset, peek.limit);
2126 self.deliver_response(ComputeControllerResponse::PeekNotification(
2129 uuid,
2130 notification,
2131 otel_ctx,
2132 ));
2133
2134 self.finish_peek(uuid, response)
2135 }
2136
2137 fn handle_copy_to_response(
2138 &mut self,
2139 sink_id: GlobalId,
2140 response: CopyToResponse,
2141 replica_id: ReplicaId,
2142 ) {
2143 if !self.collections.contains_key(&sink_id) {
2144 soft_panic_or_log!(
2145 "received response for an unknown copy-to \
2146 (sink_id={sink_id}, replica_id={replica_id})",
2147 );
2148 return;
2149 }
2150 let Some(replica) = self.replicas.get_mut(&replica_id) else {
2151 soft_panic_or_log!("copy-to response for an unknown replica (replica_id={replica_id})");
2152 return;
2153 };
2154 let Some(replica_collection) = replica.collections.get_mut(&sink_id) else {
2155 soft_panic_or_log!(
2156 "copy-to response for an unknown replica collection \
2157 (sink_id={sink_id}, replica_id={replica_id})"
2158 );
2159 return;
2160 };
2161
2162 replica_collection.update_write_frontier(Antichain::new());
2166 replica_collection.update_input_frontier(Antichain::new());
2167 replica_collection.update_output_frontier(Antichain::new());
2168
2169 if !self.copy_tos.remove(&sink_id) {
2172 return;
2173 }
2174
2175 let result = match response {
2176 CopyToResponse::RowCount(count) => Ok(count),
2177 CopyToResponse::Error(error) => Err(anyhow::anyhow!(error)),
2178 CopyToResponse::Dropped => {
2183 tracing::error!(
2184 %sink_id, %replica_id,
2185 "received `Dropped` response for a tracked copy to",
2186 );
2187 return;
2188 }
2189 };
2190
2191 self.deliver_response(ComputeControllerResponse::CopyToResponse(sink_id, result));
2192 }
2193
2194 fn handle_subscribe_response(
2195 &mut self,
2196 subscribe_id: GlobalId,
2197 response: SubscribeResponse,
2198 replica_id: ReplicaId,
2199 ) {
2200 if !self.collections.contains_key(&subscribe_id) {
2201 soft_panic_or_log!(
2202 "received response for an unknown subscribe \
2203 (subscribe_id={subscribe_id}, replica_id={replica_id})",
2204 );
2205 return;
2206 }
2207 let Some(replica) = self.replicas.get_mut(&replica_id) else {
2208 soft_panic_or_log!(
2209 "subscribe response for an unknown replica (replica_id={replica_id})"
2210 );
2211 return;
2212 };
2213 let Some(replica_collection) = replica.collections.get_mut(&subscribe_id) else {
2214 soft_panic_or_log!(
2215 "subscribe response for an unknown replica collection \
2216 (subscribe_id={subscribe_id}, replica_id={replica_id})"
2217 );
2218 return;
2219 };
2220
2221 let write_frontier = match &response {
2225 SubscribeResponse::Batch(batch) => batch.upper.clone(),
2226 SubscribeResponse::DroppedAt(_) => Antichain::new(),
2227 };
2228
2229 replica_collection.update_write_frontier(write_frontier.clone());
2233 replica_collection.update_input_frontier(write_frontier.clone());
2234 replica_collection.update_output_frontier(write_frontier.clone());
2235
2236 let Some(mut subscribe) = self.subscribes.get(&subscribe_id).cloned() else {
2238 return;
2239 };
2240
2241 self.maybe_update_global_write_frontier(subscribe_id, write_frontier);
2247
2248 match response {
2249 SubscribeResponse::Batch(batch) => {
2250 let upper = batch.upper;
2251 let mut updates = batch.updates;
2252
2253 if PartialOrder::less_than(&subscribe.frontier, &upper) {
2256 let lower = std::mem::replace(&mut subscribe.frontier, upper.clone());
2257
2258 if upper.is_empty() {
2259 self.subscribes.remove(&subscribe_id);
2261 } else {
2262 self.subscribes.insert(subscribe_id, subscribe);
2264 }
2265
2266 if let Ok(updates) = updates.as_mut() {
2267 updates.retain_mut(|updates| {
2268 let offset = updates.times().partition_point(|t| {
2269 !lower.less_equal(t)
2272 });
2273 let (_, past_lower) = std::mem::take(updates).split_at(offset);
2274 *updates = past_lower;
2275 updates.len() > 0
2276 });
2277 }
2278 self.deliver_response(ComputeControllerResponse::SubscribeResponse(
2279 subscribe_id,
2280 SubscribeBatch {
2281 lower,
2282 upper,
2283 updates,
2284 },
2285 ));
2286 }
2287 }
2288 SubscribeResponse::DroppedAt(frontier) => {
2289 tracing::error!(
2294 %subscribe_id,
2295 %replica_id,
2296 frontier = ?frontier.elements(),
2297 "received `DroppedAt` response for a tracked subscribe",
2298 );
2299 self.subscribes.remove(&subscribe_id);
2300 }
2301 }
2302 }
2303
2304 fn handle_status_response(&self, response: StatusResponse, _replica_id: ReplicaId) {
2305 match response {
2306 StatusResponse::Placeholder => {}
2307 }
2308 }
2309
2310 fn dependency_write_frontiers<'b>(
2312 &'b self,
2313 collection: &'b CollectionState,
2314 ) -> impl Iterator<Item = Antichain<Timestamp>> + 'b {
2315 let compute_frontiers = collection.compute_dependency_ids().filter_map(|dep_id| {
2316 let collection = self.collections.get(&dep_id);
2317 collection.map(|c| c.write_frontier())
2318 });
2319 let storage_frontiers = collection.storage_dependency_ids().filter_map(|dep_id| {
2320 let frontiers = self.storage_collections.collection_frontiers(dep_id).ok();
2321 frontiers.map(|f| f.write_frontier)
2322 });
2323
2324 compute_frontiers.chain(storage_frontiers)
2325 }
2326
2327 fn transitive_storage_dependency_write_frontiers<'b>(
2329 &'b self,
2330 collection: &'b CollectionState,
2331 ) -> impl Iterator<Item = Antichain<Timestamp>> + 'b {
2332 let mut storage_ids: BTreeSet<_> = collection.storage_dependency_ids().collect();
2333 let mut todo: Vec<_> = collection.compute_dependency_ids().collect();
2334 let mut done = BTreeSet::new();
2335
2336 while let Some(id) = todo.pop() {
2337 if done.contains(&id) {
2338 continue;
2339 }
2340 if let Some(dep) = self.collections.get(&id) {
2341 storage_ids.extend(dep.storage_dependency_ids());
2342 todo.extend(dep.compute_dependency_ids())
2343 }
2344 done.insert(id);
2345 }
2346
2347 let storage_frontiers = storage_ids.into_iter().filter_map(|id| {
2348 let frontiers = self.storage_collections.collection_frontiers(id).ok();
2349 frontiers.map(|f| f.write_frontier)
2350 });
2351
2352 storage_frontiers
2353 }
2354
2355 fn downgrade_warmup_capabilities(&mut self) {
2368 let mut new_capabilities = BTreeMap::new();
2369 for (id, collection) in &self.collections {
2370 if collection.read_policy.is_none()
2374 && collection.shared.lock_write_frontier(|f| f.is_empty())
2375 {
2376 new_capabilities.insert(*id, Antichain::new());
2377 continue;
2378 }
2379
2380 let mut new_capability = Antichain::new();
2381 for frontier in self.dependency_write_frontiers(collection) {
2382 for time in frontier {
2383 new_capability.insert(time.step_back().unwrap_or(time));
2384 }
2385 }
2386
2387 new_capabilities.insert(*id, new_capability);
2388 }
2389
2390 for (id, new_capability) in new_capabilities {
2391 let collection = self.expect_collection_mut(id);
2392 let _ = collection.warmup_read_hold.try_downgrade(new_capability);
2393 }
2394 }
2395
2396 fn forward_implied_capabilities(&mut self) {
2424 if !ENABLE_PAUSED_CLUSTER_READHOLD_DOWNGRADE.get(&self.dyncfg) {
2425 return;
2426 }
2427 if !self.replicas.is_empty() {
2428 return;
2429 }
2430
2431 let mut new_capabilities = BTreeMap::new();
2432 for (id, collection) in &self.collections {
2433 let Some(read_policy) = &collection.read_policy else {
2434 continue;
2436 };
2437
2438 let mut dep_frontier = Antichain::new();
2442 for frontier in self.transitive_storage_dependency_write_frontiers(collection) {
2443 dep_frontier.extend(frontier);
2444 }
2445
2446 let new_capability = read_policy.frontier(dep_frontier.borrow());
2447 if PartialOrder::less_than(collection.implied_read_hold.since(), &new_capability) {
2448 new_capabilities.insert(*id, new_capability);
2449 }
2450 }
2451
2452 for (id, new_capability) in new_capabilities {
2453 let collection = self.expect_collection_mut(id);
2454 let _ = collection.implied_read_hold.try_downgrade(new_capability);
2455 }
2456 }
2457
2458 pub(super) fn acquire_read_hold(&self, id: GlobalId) -> Result<ReadHold, CollectionMissing> {
2463 let collection = self.collection(id)?;
2469 let since = collection.shared.lock_read_capabilities(|caps| {
2470 let since = caps.frontier().to_owned();
2471 caps.update_iter(since.iter().map(|t| (t.clone(), 1)));
2472 since
2473 });
2474 let hold = ReadHold::new(id, since, Arc::clone(&self.read_hold_tx));
2475 Ok(hold)
2476 }
2477
2478 #[mz_ore::instrument(level = "debug")]
2484 pub fn maintain(&mut self) {
2485 self.rehydrate_failed_replicas();
2486 self.downgrade_warmup_capabilities();
2487 self.forward_implied_capabilities();
2488 self.schedule_collections();
2489 self.cleanup_collections();
2490 self.update_frontier_introspection();
2491 self.refresh_state_metrics();
2492 self.refresh_wallclock_lag();
2493 }
2494}
2495
2496#[derive(Debug)]
2501struct CollectionState {
2502 target_replica: Option<ReplicaId>,
2504 log_collection: bool,
2508 dropped: bool,
2514 scheduled: bool,
2517
2518 read_only: bool,
2522
2523 shared: SharedCollectionState,
2525
2526 implied_read_hold: ReadHold,
2533 warmup_read_hold: ReadHold,
2541 read_policy: Option<ReadPolicy>,
2547
2548 storage_dependencies: BTreeMap<GlobalId, ReadHold>,
2551 compute_dependencies: BTreeMap<GlobalId, ReadHold>,
2554
2555 introspection: CollectionIntrospection,
2557
2558 wallclock_lag_histogram_stash: Option<
2565 BTreeMap<
2566 (
2567 WallclockLagHistogramPeriod,
2568 WallclockLag,
2569 BTreeMap<&'static str, String>,
2570 ),
2571 Diff,
2572 >,
2573 >,
2574}
2575
2576impl CollectionState {
2577 fn new(
2579 collection_id: GlobalId,
2580 as_of: Antichain<Timestamp>,
2581 shared: SharedCollectionState,
2582 storage_dependencies: BTreeMap<GlobalId, ReadHold>,
2583 compute_dependencies: BTreeMap<GlobalId, ReadHold>,
2584 read_hold_tx: read_holds::ChangeTx,
2585 introspection: CollectionIntrospection,
2586 ) -> Self {
2587 let since = as_of.clone();
2589 let upper = as_of;
2591
2592 assert!(shared.lock_read_capabilities(|c| c.frontier() == since.borrow()));
2594 assert!(shared.lock_write_frontier(|f| f == &upper));
2595
2596 let implied_read_hold =
2600 ReadHold::new(collection_id, since.clone(), Arc::clone(&read_hold_tx));
2601 let warmup_read_hold = ReadHold::new(collection_id, since.clone(), read_hold_tx);
2602
2603 let updates = warmup_read_hold.since().iter().map(|t| (t.clone(), 1));
2604 shared.lock_read_capabilities(|c| {
2605 c.update_iter(updates);
2606 });
2607
2608 let wallclock_lag_histogram_stash = match collection_id.is_transient() {
2612 true => None,
2613 false => Some(Default::default()),
2614 };
2615
2616 Self {
2617 target_replica: None,
2618 log_collection: false,
2619 dropped: false,
2620 scheduled: false,
2621 read_only: true,
2622 shared,
2623 implied_read_hold,
2624 warmup_read_hold,
2625 read_policy: Some(ReadPolicy::ValidFrom(since)),
2626 storage_dependencies,
2627 compute_dependencies,
2628 introspection,
2629 wallclock_lag_histogram_stash,
2630 }
2631 }
2632
2633 fn new_log_collection(
2635 id: GlobalId,
2636 shared: SharedCollectionState,
2637 read_hold_tx: read_holds::ChangeTx,
2638 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
2639 ) -> Self {
2640 let since = Antichain::from_elem(Timestamp::MIN);
2641 let introspection = CollectionIntrospection::new(
2642 id,
2643 introspection_tx,
2644 since.clone(),
2645 false,
2646 None,
2647 None,
2648 Vec::new(),
2649 );
2650 let mut state = Self::new(
2651 id,
2652 since,
2653 shared,
2654 Default::default(),
2655 Default::default(),
2656 read_hold_tx,
2657 introspection,
2658 );
2659 state.log_collection = true;
2660 state.scheduled = true;
2662 state
2663 }
2664
2665 fn read_frontier(&self) -> Antichain<Timestamp> {
2667 self.shared
2668 .lock_read_capabilities(|c| c.frontier().to_owned())
2669 }
2670
2671 fn write_frontier(&self) -> Antichain<Timestamp> {
2673 self.shared.lock_write_frontier(|f| f.clone())
2674 }
2675
2676 fn storage_dependency_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
2677 self.storage_dependencies.keys().copied()
2678 }
2679
2680 fn compute_dependency_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
2681 self.compute_dependencies.keys().copied()
2682 }
2683}
2684
2685#[derive(Clone, Debug)]
2696pub(super) struct SharedCollectionState {
2697 read_capabilities: Arc<Mutex<MutableAntichain<Timestamp>>>,
2710 write_frontier: Arc<Mutex<Antichain<Timestamp>>>,
2712}
2713
2714impl SharedCollectionState {
2715 pub fn new(as_of: Antichain<Timestamp>) -> Self {
2716 let since = as_of.clone();
2718 let upper = as_of;
2720
2721 let mut read_capabilities = MutableAntichain::new();
2725 read_capabilities.update_iter(since.iter().map(|time| (time.clone(), 1)));
2726
2727 Self {
2728 read_capabilities: Arc::new(Mutex::new(read_capabilities)),
2729 write_frontier: Arc::new(Mutex::new(upper)),
2730 }
2731 }
2732
2733 pub fn lock_read_capabilities<F, R>(&self, f: F) -> R
2734 where
2735 F: FnOnce(&mut MutableAntichain<Timestamp>) -> R,
2736 {
2737 let mut caps = self.read_capabilities.lock().expect("poisoned");
2738 f(&mut *caps)
2739 }
2740
2741 pub fn lock_write_frontier<F, R>(&self, f: F) -> R
2742 where
2743 F: FnOnce(&mut Antichain<Timestamp>) -> R,
2744 {
2745 let mut frontier = self.write_frontier.lock().expect("poisoned");
2746 f(&mut *frontier)
2747 }
2748}
2749
2750#[derive(Debug)]
2753struct CollectionIntrospection {
2754 collection_id: GlobalId,
2756 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
2758 frontiers: Option<FrontiersIntrospectionState>,
2763 refresh: Option<RefreshIntrospectionState>,
2767 dependency_ids: Vec<GlobalId>,
2769}
2770
2771impl CollectionIntrospection {
2772 fn new(
2773 collection_id: GlobalId,
2774 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
2775 as_of: Antichain<Timestamp>,
2776 storage_sink: bool,
2777 initial_as_of: Option<Antichain<Timestamp>>,
2778 refresh_schedule: Option<RefreshSchedule>,
2779 dependency_ids: Vec<GlobalId>,
2780 ) -> Self {
2781 let refresh =
2782 match (refresh_schedule, initial_as_of) {
2783 (Some(refresh_schedule), Some(initial_as_of)) => Some(
2784 RefreshIntrospectionState::new(refresh_schedule, initial_as_of, &as_of),
2785 ),
2786 (refresh_schedule, _) => {
2787 soft_assert_or_log!(
2790 refresh_schedule.is_none(),
2791 "`refresh_schedule` without an `initial_as_of`: {collection_id}"
2792 );
2793 None
2794 }
2795 };
2796 let frontiers = (!storage_sink).then(|| FrontiersIntrospectionState::new(as_of));
2797
2798 let self_ = Self {
2799 collection_id,
2800 introspection_tx,
2801 frontiers,
2802 refresh,
2803 dependency_ids,
2804 };
2805
2806 self_.report_initial_state();
2807 self_
2808 }
2809
2810 fn report_initial_state(&self) {
2812 if let Some(frontiers) = &self.frontiers {
2813 let row = frontiers.row_for_collection(self.collection_id);
2814 let updates = vec![(row, Diff::ONE)];
2815 self.send(IntrospectionType::Frontiers, updates);
2816 }
2817
2818 if let Some(refresh) = &self.refresh {
2819 let row = refresh.row_for_collection(self.collection_id);
2820 let updates = vec![(row, Diff::ONE)];
2821 self.send(IntrospectionType::ComputeMaterializedViewRefreshes, updates);
2822 }
2823
2824 if !self.dependency_ids.is_empty() {
2825 let updates = self.dependency_rows(Diff::ONE);
2826 self.send(IntrospectionType::ComputeDependencies, updates);
2827 }
2828 }
2829
2830 fn dependency_rows(&self, diff: Diff) -> Vec<(Row, Diff)> {
2832 self.dependency_ids
2833 .iter()
2834 .map(|dependency_id| {
2835 let row = Row::pack_slice(&[
2836 Datum::String(&self.collection_id.to_string()),
2837 Datum::String(&dependency_id.to_string()),
2838 ]);
2839 (row, diff)
2840 })
2841 .collect()
2842 }
2843
2844 fn observe_frontiers(
2847 &mut self,
2848 read_frontier: &Antichain<Timestamp>,
2849 write_frontier: &Antichain<Timestamp>,
2850 ) {
2851 self.update_frontier_introspection(read_frontier, write_frontier);
2852 self.update_refresh_introspection(write_frontier);
2853 }
2854
2855 fn update_frontier_introspection(
2856 &mut self,
2857 read_frontier: &Antichain<Timestamp>,
2858 write_frontier: &Antichain<Timestamp>,
2859 ) {
2860 let Some(frontiers) = &mut self.frontiers else {
2861 return;
2862 };
2863
2864 if &frontiers.read_frontier == read_frontier && &frontiers.write_frontier == write_frontier
2865 {
2866 return; };
2868
2869 let retraction = frontiers.row_for_collection(self.collection_id);
2870 frontiers.update(read_frontier, write_frontier);
2871 let insertion = frontiers.row_for_collection(self.collection_id);
2872 let updates = vec![(retraction, Diff::MINUS_ONE), (insertion, Diff::ONE)];
2873 self.send(IntrospectionType::Frontiers, updates);
2874 }
2875
2876 fn update_refresh_introspection(&mut self, write_frontier: &Antichain<Timestamp>) {
2877 let Some(refresh) = &mut self.refresh else {
2878 return;
2879 };
2880
2881 let retraction = refresh.row_for_collection(self.collection_id);
2882 refresh.frontier_update(write_frontier);
2883 let insertion = refresh.row_for_collection(self.collection_id);
2884
2885 if retraction == insertion {
2886 return; }
2888
2889 let updates = vec![(retraction, Diff::MINUS_ONE), (insertion, Diff::ONE)];
2890 self.send(IntrospectionType::ComputeMaterializedViewRefreshes, updates);
2891 }
2892
2893 fn send(&self, introspection_type: IntrospectionType, updates: Vec<(Row, Diff)>) {
2894 let _ = self.introspection_tx.send((introspection_type, updates));
2897 }
2898}
2899
2900impl Drop for CollectionIntrospection {
2901 fn drop(&mut self) {
2902 if let Some(frontiers) = &self.frontiers {
2904 let row = frontiers.row_for_collection(self.collection_id);
2905 let updates = vec![(row, Diff::MINUS_ONE)];
2906 self.send(IntrospectionType::Frontiers, updates);
2907 }
2908
2909 if let Some(refresh) = &self.refresh {
2911 let retraction = refresh.row_for_collection(self.collection_id);
2912 let updates = vec![(retraction, Diff::MINUS_ONE)];
2913 self.send(IntrospectionType::ComputeMaterializedViewRefreshes, updates);
2914 }
2915
2916 if !self.dependency_ids.is_empty() {
2918 let updates = self.dependency_rows(Diff::MINUS_ONE);
2919 self.send(IntrospectionType::ComputeDependencies, updates);
2920 }
2921 }
2922}
2923
2924#[derive(Debug)]
2925struct FrontiersIntrospectionState {
2926 read_frontier: Antichain<Timestamp>,
2927 write_frontier: Antichain<Timestamp>,
2928}
2929
2930impl FrontiersIntrospectionState {
2931 fn new(as_of: Antichain<Timestamp>) -> Self {
2932 Self {
2933 read_frontier: as_of.clone(),
2934 write_frontier: as_of,
2935 }
2936 }
2937
2938 fn row_for_collection(&self, collection_id: GlobalId) -> Row {
2940 let read_frontier = self
2941 .read_frontier
2942 .as_option()
2943 .map_or(Datum::Null, |ts| ts.clone().into());
2944 let write_frontier = self
2945 .write_frontier
2946 .as_option()
2947 .map_or(Datum::Null, |ts| ts.clone().into());
2948 Row::pack_slice(&[
2949 Datum::String(&collection_id.to_string()),
2950 read_frontier,
2951 write_frontier,
2952 ])
2953 }
2954
2955 fn update(
2957 &mut self,
2958 read_frontier: &Antichain<Timestamp>,
2959 write_frontier: &Antichain<Timestamp>,
2960 ) {
2961 if read_frontier != &self.read_frontier {
2962 self.read_frontier.clone_from(read_frontier);
2963 }
2964 if write_frontier != &self.write_frontier {
2965 self.write_frontier.clone_from(write_frontier);
2966 }
2967 }
2968}
2969
2970#[derive(Debug)]
2973struct RefreshIntrospectionState {
2974 refresh_schedule: RefreshSchedule,
2976 initial_as_of: Antichain<Timestamp>,
2977 next_refresh: Datum<'static>, last_completed_refresh: Datum<'static>, }
2981
2982impl RefreshIntrospectionState {
2983 fn row_for_collection(&self, collection_id: GlobalId) -> Row {
2985 Row::pack_slice(&[
2986 Datum::String(&collection_id.to_string()),
2987 self.last_completed_refresh,
2988 self.next_refresh,
2989 ])
2990 }
2991}
2992
2993impl RefreshIntrospectionState {
2994 fn new(
2997 refresh_schedule: RefreshSchedule,
2998 initial_as_of: Antichain<Timestamp>,
2999 upper: &Antichain<Timestamp>,
3000 ) -> Self {
3001 let mut self_ = Self {
3002 refresh_schedule: refresh_schedule.clone(),
3003 initial_as_of: initial_as_of.clone(),
3004 next_refresh: Datum::Null,
3005 last_completed_refresh: Datum::Null,
3006 };
3007 self_.frontier_update(upper);
3008 self_
3009 }
3010
3011 fn frontier_update(&mut self, write_frontier: &Antichain<Timestamp>) {
3014 if write_frontier.is_empty() {
3015 self.last_completed_refresh =
3016 if let Some(last_refresh) = self.refresh_schedule.last_refresh() {
3017 last_refresh.into()
3018 } else {
3019 Timestamp::MAX.into()
3022 };
3023 self.next_refresh = Datum::Null;
3024 } else {
3025 if PartialOrder::less_equal(write_frontier, &self.initial_as_of) {
3026 self.last_completed_refresh = Datum::Null;
3028 let initial_as_of = self.initial_as_of.as_option().expect(
3029 "initial_as_of can't be [], because then there would be no refreshes at all",
3030 );
3031 let first_refresh = self
3032 .refresh_schedule
3033 .round_up_timestamp(*initial_as_of)
3034 .expect("sequencing makes sure that REFRESH MVs always have a first refresh");
3035 soft_assert_or_log!(
3036 first_refresh == *initial_as_of,
3037 "initial_as_of should be set to the first refresh"
3038 );
3039 self.next_refresh = first_refresh.into();
3040 } else {
3041 let write_frontier = write_frontier.as_option().expect("checked above");
3043 self.last_completed_refresh = self
3044 .refresh_schedule
3045 .round_down_timestamp_m1(*write_frontier)
3046 .map_or_else(
3047 || {
3048 soft_panic_or_log!(
3049 "rounding down should have returned the first refresh or later"
3050 );
3051 Datum::Null
3052 },
3053 |last_completed_refresh| last_completed_refresh.into(),
3054 );
3055 self.next_refresh = write_frontier.clone().into();
3056 }
3057 }
3058 }
3059}
3060
3061#[derive(Debug)]
3063struct PendingPeek {
3064 target_replica: Option<ReplicaId>,
3068 otel_ctx: OpenTelemetryContext,
3070 requested_at: Instant,
3074 read_hold: ReadHold,
3076 peek_response_tx: oneshot::Sender<PeekResponse>,
3078 limit: Option<usize>,
3080 offset: usize,
3082}
3083
3084#[derive(Debug, Clone)]
3085struct ActiveSubscribe {
3086 frontier: Antichain<Timestamp>,
3088}
3089
3090impl Default for ActiveSubscribe {
3091 fn default() -> Self {
3092 Self {
3093 frontier: Antichain::from_elem(Timestamp::MIN),
3094 }
3095 }
3096}
3097
3098#[derive(Debug)]
3100struct ReplicaState {
3101 id: ReplicaId,
3103 client: ReplicaClient,
3105 config: ReplicaConfig,
3107 metrics: ReplicaMetrics,
3109 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3111 collections: BTreeMap<GlobalId, ReplicaCollectionState>,
3113 epoch: u64,
3115}
3116
3117impl ReplicaState {
3118 fn new(
3119 id: ReplicaId,
3120 client: ReplicaClient,
3121 config: ReplicaConfig,
3122 metrics: ReplicaMetrics,
3123 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3124 epoch: u64,
3125 ) -> Self {
3126 Self {
3127 id,
3128 client,
3129 config,
3130 metrics,
3131 introspection_tx,
3132 epoch,
3133 collections: Default::default(),
3134 }
3135 }
3136
3137 fn add_collection(
3143 &mut self,
3144 id: GlobalId,
3145 as_of: Antichain<Timestamp>,
3146 input_read_holds: Vec<ReadHold>,
3147 ) {
3148 let metrics = self.metrics.for_collection(id);
3149 let introspection = ReplicaCollectionIntrospection::new(
3150 self.id,
3151 id,
3152 self.introspection_tx.clone(),
3153 as_of.clone(),
3154 );
3155 let mut state =
3156 ReplicaCollectionState::new(metrics, as_of, introspection, input_read_holds);
3157
3158 if id.is_transient() {
3162 state.wallclock_lag_max = None;
3163 }
3164
3165 if let Some(previous) = self.collections.insert(id, state) {
3166 panic!("attempt to add a collection with existing ID {id} (previous={previous:?}");
3167 }
3168 }
3169
3170 fn remove_collection(&mut self, id: GlobalId) -> Option<ReplicaCollectionState> {
3172 self.collections.remove(&id)
3173 }
3174
3175 fn collection_frontiers_empty(&self, id: GlobalId) -> bool {
3177 self.collections.get(&id).map_or(true, |c| {
3178 c.write_frontier.is_empty()
3179 && c.input_frontier.is_empty()
3180 && c.output_frontier.is_empty()
3181 })
3182 }
3183
3184 #[mz_ore::instrument(level = "debug")]
3188 pub fn dump(&self) -> Result<serde_json::Value, anyhow::Error> {
3189 let Self {
3196 id,
3197 client: _,
3198 config: _,
3199 metrics: _,
3200 introspection_tx: _,
3201 epoch,
3202 collections,
3203 } = self;
3204
3205 let collections: BTreeMap<_, _> = collections
3206 .iter()
3207 .map(|(id, collection)| (id.to_string(), format!("{collection:?}")))
3208 .collect();
3209
3210 Ok(serde_json::json!({
3211 "id": id.to_string(),
3212 "collections": collections,
3213 "epoch": epoch,
3214 }))
3215 }
3216}
3217
3218#[derive(Debug)]
3219struct ReplicaCollectionState {
3220 write_frontier: Antichain<Timestamp>,
3224 input_frontier: Antichain<Timestamp>,
3228 output_frontier: Antichain<Timestamp>,
3232
3233 metrics: Option<ReplicaCollectionMetrics>,
3237 as_of: Antichain<Timestamp>,
3239 introspection: ReplicaCollectionIntrospection,
3241 input_read_holds: Vec<ReadHold>,
3247
3248 wallclock_lag_max: Option<WallclockLag>,
3252}
3253
3254impl ReplicaCollectionState {
3255 fn new(
3256 metrics: Option<ReplicaCollectionMetrics>,
3257 as_of: Antichain<Timestamp>,
3258 introspection: ReplicaCollectionIntrospection,
3259 input_read_holds: Vec<ReadHold>,
3260 ) -> Self {
3261 Self {
3262 write_frontier: as_of.clone(),
3263 input_frontier: as_of.clone(),
3264 output_frontier: as_of.clone(),
3265 metrics,
3266 as_of,
3267 introspection,
3268 input_read_holds,
3269 wallclock_lag_max: Some(WallclockLag::MIN),
3270 }
3271 }
3272
3273 fn hydrated(&self) -> bool {
3275 self.as_of.is_empty() || PartialOrder::less_than(&self.as_of, &self.output_frontier)
3291 }
3292
3293 fn update_write_frontier(&mut self, new_frontier: Antichain<Timestamp>) {
3295 if PartialOrder::less_than(&new_frontier, &self.write_frontier) {
3296 soft_panic_or_log!(
3297 "replica collection write frontier regression (old={:?}, new={new_frontier:?})",
3298 self.write_frontier,
3299 );
3300 return;
3301 } else if new_frontier == self.write_frontier {
3302 return;
3303 }
3304
3305 self.write_frontier = new_frontier;
3306 }
3307
3308 fn update_input_frontier(&mut self, new_frontier: Antichain<Timestamp>) {
3310 if PartialOrder::less_than(&new_frontier, &self.input_frontier) {
3311 soft_panic_or_log!(
3312 "replica collection input frontier regression (old={:?}, new={new_frontier:?})",
3313 self.input_frontier,
3314 );
3315 return;
3316 } else if new_frontier == self.input_frontier {
3317 return;
3318 }
3319
3320 self.input_frontier = new_frontier;
3321
3322 for read_hold in &mut self.input_read_holds {
3324 let result = read_hold.try_downgrade(self.input_frontier.clone());
3325 soft_assert_or_log!(
3326 result.is_ok(),
3327 "read hold downgrade failed (read_hold={read_hold:?}, new_since={:?})",
3328 self.input_frontier,
3329 );
3330 }
3331 }
3332
3333 fn update_output_frontier(&mut self, new_frontier: Antichain<Timestamp>) {
3335 if PartialOrder::less_than(&new_frontier, &self.output_frontier) {
3336 soft_panic_or_log!(
3337 "replica collection output frontier regression (old={:?}, new={new_frontier:?})",
3338 self.output_frontier,
3339 );
3340 return;
3341 } else if new_frontier == self.output_frontier {
3342 return;
3343 }
3344
3345 self.output_frontier = new_frontier;
3346 }
3347}
3348
3349#[derive(Debug)]
3352struct ReplicaCollectionIntrospection {
3353 replica_id: ReplicaId,
3355 collection_id: GlobalId,
3357 write_frontier: Antichain<Timestamp>,
3359 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3361}
3362
3363impl ReplicaCollectionIntrospection {
3364 fn new(
3366 replica_id: ReplicaId,
3367 collection_id: GlobalId,
3368 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3369 as_of: Antichain<Timestamp>,
3370 ) -> Self {
3371 let self_ = Self {
3372 replica_id,
3373 collection_id,
3374 write_frontier: as_of,
3375 introspection_tx,
3376 };
3377
3378 self_.report_initial_state();
3379 self_
3380 }
3381
3382 fn report_initial_state(&self) {
3384 let row = self.write_frontier_row();
3385 let updates = vec![(row, Diff::ONE)];
3386 self.send(IntrospectionType::ReplicaFrontiers, updates);
3387 }
3388
3389 fn observe_frontier(&mut self, write_frontier: &Antichain<Timestamp>) {
3391 if self.write_frontier == *write_frontier {
3392 return; }
3394
3395 let retraction = self.write_frontier_row();
3396 self.write_frontier.clone_from(write_frontier);
3397 let insertion = self.write_frontier_row();
3398
3399 let updates = vec![(retraction, Diff::MINUS_ONE), (insertion, Diff::ONE)];
3400 self.send(IntrospectionType::ReplicaFrontiers, updates);
3401 }
3402
3403 fn write_frontier_row(&self) -> Row {
3405 let write_frontier = self
3406 .write_frontier
3407 .as_option()
3408 .map_or(Datum::Null, |ts| ts.clone().into());
3409 Row::pack_slice(&[
3410 Datum::String(&self.collection_id.to_string()),
3411 Datum::String(&self.replica_id.to_string()),
3412 write_frontier,
3413 ])
3414 }
3415
3416 fn send(&self, introspection_type: IntrospectionType, updates: Vec<(Row, Diff)>) {
3417 let _ = self.introspection_tx.send((introspection_type, updates));
3420 }
3421}
3422
3423impl Drop for ReplicaCollectionIntrospection {
3424 fn drop(&mut self) {
3425 let row = self.write_frontier_row();
3427 let updates = vec![(row, Diff::MINUS_ONE)];
3428 self.send(IntrospectionType::ReplicaFrontiers, updates);
3429 }
3430}
3431
3432#[cfg(test)]
3433mod tests {
3434 use std::collections::BTreeMap;
3435
3436 use mz_compute_types::dyncfgs::{ENABLE_COLUMN_PAGED_BATCHER, ENABLE_MZ_JOIN_CORE};
3437 use mz_dyncfg::{ConfigSet, ConfigUpdates, ConfigVal};
3438 use mz_persist_types::PersistLocation;
3439
3440 use crate::protocol::command::{ComputeCommand, InstanceConfig};
3441
3442 use super::{Instance, ReplicaId};
3443
3444 fn create_instance_command() -> ComputeCommand {
3445 ComputeCommand::CreateInstance(Box::new(InstanceConfig {
3446 logging: Default::default(),
3447 expiration_offset: None,
3448 peek_stash_persist_location: PersistLocation::new_in_mem(),
3449 arrangement_dictionary_compression: false,
3450 initial_config: Default::default(),
3451 }))
3452 }
3453
3454 fn initial_config(cmd: &ComputeCommand) -> &ConfigUpdates {
3455 match cmd {
3456 ComputeCommand::CreateInstance(config) => &config.initial_config,
3457 other => panic!("expected CreateInstance, got {other:?}"),
3458 }
3459 }
3460
3461 #[mz_ore::test]
3466 fn create_instance_snapshots_instance_wide_dyncfg() {
3467 let dyncfg = ConfigSet::default()
3468 .add(&ENABLE_COLUMN_PAGED_BATCHER)
3469 .add(&ENABLE_MZ_JOIN_CORE);
3470 let mut updates = ConfigUpdates::default();
3471 updates.add(&ENABLE_COLUMN_PAGED_BATCHER, true);
3472 updates.add(&ENABLE_MZ_JOIN_CORE, false);
3473 updates.apply(&dyncfg);
3474
3475 let overrides = BTreeMap::new();
3477 let cmd = Instance::specialize_command_for_replica(
3478 create_instance_command(),
3479 ReplicaId::User(1),
3480 &overrides,
3481 &dyncfg,
3482 );
3483 let snapshot = initial_config(&cmd);
3484 assert_eq!(
3485 snapshot.updates.get(ENABLE_COLUMN_PAGED_BATCHER.name()),
3486 Some(&ConfigVal::Bool(true)),
3487 );
3488 assert_eq!(
3489 snapshot.updates.get(ENABLE_MZ_JOIN_CORE.name()),
3490 Some(&ConfigVal::Bool(false)),
3491 );
3492 }
3493
3494 #[mz_ore::test]
3497 fn create_instance_snapshot_applies_replica_override() {
3498 let dyncfg = ConfigSet::default().add(&ENABLE_COLUMN_PAGED_BATCHER);
3499 let mut updates = ConfigUpdates::default();
3500 updates.add(&ENABLE_COLUMN_PAGED_BATCHER, true);
3501 updates.apply(&dyncfg);
3502
3503 let replica = ReplicaId::User(1);
3504 let mut override_updates = ConfigUpdates::default();
3505 override_updates.add(&ENABLE_COLUMN_PAGED_BATCHER, false);
3506 let overrides = BTreeMap::from([(replica, override_updates)]);
3507
3508 let cmd = Instance::specialize_command_for_replica(
3509 create_instance_command(),
3510 replica,
3511 &overrides,
3512 &dyncfg,
3513 );
3514 assert_eq!(
3515 initial_config(&cmd)
3516 .updates
3517 .get(ENABLE_COLUMN_PAGED_BATCHER.name()),
3518 Some(&ConfigVal::Bool(false)),
3519 "replica override should win over the instance-wide value",
3520 );
3521 }
3522
3523 #[mz_ore::test]
3525 fn update_configuration_merges_replica_override() {
3526 let dyncfg = ConfigSet::default().add(&ENABLE_COLUMN_PAGED_BATCHER);
3527
3528 let replica = ReplicaId::User(1);
3529 let mut override_updates = ConfigUpdates::default();
3530 override_updates.add(&ENABLE_COLUMN_PAGED_BATCHER, true);
3531 let overrides = BTreeMap::from([(replica, override_updates)]);
3532
3533 let cmd = Instance::specialize_command_for_replica(
3534 ComputeCommand::UpdateConfiguration(Box::new(Default::default())),
3535 replica,
3536 &overrides,
3537 &dyncfg,
3538 );
3539 match cmd {
3540 ComputeCommand::UpdateConfiguration(params) => assert_eq!(
3541 params
3542 .dyncfg_updates
3543 .updates
3544 .get(ENABLE_COLUMN_PAGED_BATCHER.name()),
3545 Some(&ConfigVal::Bool(true)),
3546 ),
3547 other => panic!("expected UpdateConfiguration, got {other:?}"),
3548 }
3549 }
3550}