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, PeekResponse, StatusResponse,
68 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 = PeekResponse::Error(ERROR_TARGET_REPLICA_FAILED.into());
1366 self.finish_peek(uuid, response);
1367 }
1368
1369 self.forward_implied_capabilities();
1372
1373 Ok(())
1374 }
1375
1376 fn rehydrate_replica(&mut self, id: ReplicaId) {
1382 let config = self.replicas[&id].config.clone();
1383 let epoch = self.replicas[&id].epoch + 1;
1384
1385 self.remove_replica(id).expect("replica must exist");
1386 let result = self.add_replica(id, config, Some(epoch));
1387
1388 match result {
1389 Ok(()) => (),
1390 Err(ReplicaExists(_)) => unreachable!("replica was removed"),
1391 }
1392 }
1393
1394 fn rehydrate_failed_replicas(&mut self) {
1396 let replicas = self.replicas.iter();
1397 let failed_replicas: Vec<_> = replicas
1398 .filter_map(|(id, replica)| replica.client.is_failed().then_some(*id))
1399 .collect();
1400
1401 for replica_id in failed_replicas {
1402 self.rehydrate_replica(replica_id);
1403 }
1404 }
1405
1406 #[mz_ore::instrument(level = "debug")]
1411 pub fn create_dataflow(
1412 &mut self,
1413 dataflow: DataflowDescription<mz_compute_types::plan::LirRelationExpr, ()>,
1414 import_read_holds: Vec<ReadHold>,
1415 mut shared_collection_state: BTreeMap<GlobalId, SharedCollectionState>,
1416 target_replica: Option<ReplicaId>,
1417 ) -> Result<(), DataflowCreationError> {
1418 use DataflowCreationError::*;
1419
1420 if let Some(replica_id) = target_replica {
1424 if !self.replica_exists(replica_id) {
1425 return Err(ReplicaMissing(replica_id));
1426 }
1427 }
1428
1429 let as_of = dataflow.as_of.as_ref().ok_or(MissingAsOf)?;
1431 if as_of.is_empty() && dataflow.subscribe_ids().next().is_some() {
1432 return Err(EmptyAsOfForSubscribe);
1433 }
1434 if as_of.is_empty() && dataflow.copy_to_ids().next().is_some() {
1435 return Err(EmptyAsOfForCopyTo);
1436 }
1437
1438 let mut storage_dependencies = BTreeMap::new();
1440 let mut compute_dependencies = BTreeMap::new();
1441
1442 let mut replica_input_read_holds = Vec::new();
1447
1448 let mut import_read_holds: BTreeMap<_, _> =
1449 import_read_holds.into_iter().map(|r| (r.id(), r)).collect();
1450
1451 for &id in dataflow.source_imports.keys() {
1452 let mut read_hold = import_read_holds.remove(&id).ok_or(ReadHoldMissing(id))?;
1453 replica_input_read_holds.push(read_hold.clone());
1454
1455 read_hold
1456 .try_downgrade(as_of.clone())
1457 .map_err(|_| ReadHoldInsufficient(id))?;
1458 storage_dependencies.insert(id, read_hold);
1459 }
1460
1461 for &id in dataflow.index_imports.keys() {
1462 let mut read_hold = import_read_holds.remove(&id).ok_or(ReadHoldMissing(id))?;
1463 read_hold
1464 .try_downgrade(as_of.clone())
1465 .map_err(|_| ReadHoldInsufficient(id))?;
1466 compute_dependencies.insert(id, read_hold);
1467 }
1468
1469 if as_of.is_empty() {
1472 replica_input_read_holds = Default::default();
1473 }
1474
1475 for export_id in dataflow.export_ids() {
1477 let shared = shared_collection_state
1478 .remove(&export_id)
1479 .unwrap_or_else(|| SharedCollectionState::new(as_of.clone()));
1480 let write_only = dataflow.sink_exports.contains_key(&export_id);
1481 let storage_sink = dataflow.persist_sink_ids().any(|id| id == export_id);
1482
1483 self.add_collection(
1484 export_id,
1485 as_of.clone(),
1486 shared,
1487 storage_dependencies.clone(),
1488 compute_dependencies.clone(),
1489 replica_input_read_holds.clone(),
1490 write_only,
1491 storage_sink,
1492 dataflow.initial_storage_as_of.clone(),
1493 dataflow.refresh_schedule.clone(),
1494 target_replica,
1495 );
1496
1497 if let Ok(frontiers) = self.storage_collections.collection_frontiers(export_id) {
1500 self.maybe_update_global_write_frontier(export_id, frontiers.write_frontier);
1501 }
1502 }
1503
1504 for subscribe_id in dataflow.subscribe_ids() {
1506 self.subscribes
1507 .insert(subscribe_id, ActiveSubscribe::default());
1508 }
1509
1510 for copy_to_id in dataflow.copy_to_ids() {
1512 self.copy_tos.insert(copy_to_id);
1513 }
1514
1515 let mut source_imports = BTreeMap::new();
1518 for (id, import) in dataflow.source_imports {
1519 let frontiers = self
1520 .storage_collections
1521 .collection_frontiers(id)
1522 .expect("collection exists");
1523
1524 let collection_metadata = self
1525 .storage_collections
1526 .collection_metadata(id)
1527 .expect("we have a read hold on this collection");
1528
1529 let desc = SourceInstanceDesc {
1530 storage_metadata: collection_metadata.clone(),
1531 arguments: import.desc.arguments,
1532 typ: import.desc.typ.clone(),
1533 };
1534 source_imports.insert(
1535 id,
1536 mz_compute_types::dataflows::SourceImport {
1537 desc,
1538 monotonic: import.monotonic,
1539 with_snapshot: import.with_snapshot,
1540 upper: frontiers.write_frontier,
1541 },
1542 );
1543 }
1544
1545 let mut sink_exports = BTreeMap::new();
1546 for (id, se) in dataflow.sink_exports {
1547 let connection = match se.connection {
1548 ComputeSinkConnection::MaterializedView(conn) => {
1549 let metadata = self
1550 .storage_collections
1551 .collection_metadata(id)
1552 .map_err(|_| CollectionMissing(id))?
1553 .clone();
1554 let conn = MaterializedViewSinkConnection {
1555 value_desc: conn.value_desc,
1556 storage_metadata: metadata,
1557 };
1558 ComputeSinkConnection::MaterializedView(conn)
1559 }
1560 ComputeSinkConnection::Subscribe(conn) => ComputeSinkConnection::Subscribe(conn),
1561 ComputeSinkConnection::CopyToS3Oneshot(conn) => {
1562 ComputeSinkConnection::CopyToS3Oneshot(conn)
1563 }
1564 ComputeSinkConnection::MetricSink(conn) => ComputeSinkConnection::MetricSink(conn),
1565 };
1566 let desc = ComputeSinkDesc {
1567 from: se.from,
1568 from_desc: se.from_desc,
1569 connection,
1570 with_snapshot: se.with_snapshot,
1571 up_to: se.up_to,
1572 non_null_assertions: se.non_null_assertions,
1573 refresh_schedule: se.refresh_schedule,
1574 };
1575 sink_exports.insert(id, desc);
1576 }
1577
1578 let objects_to_build = dataflow
1580 .objects_to_build
1581 .into_iter()
1582 .map(|object| BuildDesc {
1583 id: object.id,
1584 plan: RenderPlan::try_from(object.plan).expect("valid plan"),
1585 })
1586 .collect();
1587
1588 let augmented_dataflow = DataflowDescription {
1589 source_imports,
1590 sink_exports,
1591 objects_to_build,
1592 index_imports: dataflow.index_imports,
1594 index_exports: dataflow.index_exports,
1595 as_of: dataflow.as_of.clone(),
1596 until: dataflow.until,
1597 initial_storage_as_of: dataflow.initial_storage_as_of,
1598 refresh_schedule: dataflow.refresh_schedule,
1599 debug_name: dataflow.debug_name,
1600 time_dependence: dataflow.time_dependence,
1601 };
1602
1603 if augmented_dataflow.is_transient() {
1604 tracing::debug!(
1605 name = %augmented_dataflow.debug_name,
1606 import_ids = %augmented_dataflow.display_import_ids(),
1607 export_ids = %augmented_dataflow.display_export_ids(),
1608 as_of = ?augmented_dataflow.as_of.as_ref().unwrap().elements(),
1609 until = ?augmented_dataflow.until.elements(),
1610 "creating dataflow",
1611 );
1612 } else {
1613 tracing::info!(
1614 name = %augmented_dataflow.debug_name,
1615 import_ids = %augmented_dataflow.display_import_ids(),
1616 export_ids = %augmented_dataflow.display_export_ids(),
1617 as_of = ?augmented_dataflow.as_of.as_ref().unwrap().elements(),
1618 until = ?augmented_dataflow.until.elements(),
1619 "creating dataflow",
1620 );
1621 }
1622
1623 if as_of.is_empty() {
1626 tracing::info!(
1627 name = %augmented_dataflow.debug_name,
1628 "not sending `CreateDataflow`, because of empty `as_of`",
1629 );
1630 } else {
1631 let collections: Vec<_> = augmented_dataflow.export_ids().collect();
1632 self.send(ComputeCommand::CreateDataflow(Box::new(augmented_dataflow)));
1633
1634 for id in collections {
1635 self.maybe_schedule_collection(id);
1636 }
1637 }
1638
1639 Ok(())
1640 }
1641
1642 fn maybe_schedule_collection(&mut self, id: GlobalId) {
1648 let collection = self.expect_collection(id);
1649
1650 if collection.scheduled {
1652 return;
1653 }
1654
1655 let as_of = collection.read_frontier();
1656
1657 if as_of.is_empty() {
1660 return;
1661 }
1662
1663 let ready = if id.is_transient() {
1664 true
1670 } else {
1671 let not_self_dep = |x: &GlobalId| *x != id;
1677
1678 let mut deps_scheduled = true;
1681
1682 let compute_deps = collection.compute_dependency_ids().filter(not_self_dep);
1687 let mut compute_frontiers = Vec::new();
1688 for id in compute_deps {
1689 let dep = &self.expect_collection(id);
1690 deps_scheduled &= dep.scheduled;
1691 compute_frontiers.push(dep.write_frontier());
1692 }
1693
1694 let storage_deps = collection.storage_dependency_ids().filter(not_self_dep);
1695 let storage_frontiers = self
1696 .storage_collections
1697 .collections_frontiers(storage_deps.collect())
1698 .expect("must exist");
1699 let storage_frontiers = storage_frontiers.into_iter().map(|f| f.write_frontier);
1700
1701 let mut frontiers = compute_frontiers.into_iter().chain(storage_frontiers);
1702 let frontiers_ready =
1703 frontiers.all(|frontier| PartialOrder::less_than(&as_of, &frontier));
1704
1705 deps_scheduled && frontiers_ready
1706 };
1707
1708 if ready {
1709 self.send(ComputeCommand::Schedule(id));
1710 let collection = self.expect_collection_mut(id);
1711 collection.scheduled = true;
1712 }
1713 }
1714
1715 fn schedule_collections(&mut self) {
1717 let ids: Vec<_> = self.collections.keys().copied().collect();
1718 for id in ids {
1719 self.maybe_schedule_collection(id);
1720 }
1721 }
1722
1723 #[mz_ore::instrument(level = "debug")]
1726 pub fn drop_collections(&mut self, ids: Vec<GlobalId>) -> Result<(), CollectionMissing> {
1727 for id in &ids {
1728 let collection = self.collection_mut(*id)?;
1729
1730 collection.dropped = true;
1732
1733 collection.implied_read_hold.release();
1736 collection.warmup_read_hold.release();
1737
1738 self.subscribes.remove(id);
1741 self.copy_tos.remove(id);
1744 }
1745
1746 Ok(())
1747 }
1748
1749 #[mz_ore::instrument(level = "debug")]
1753 pub fn peek(
1754 &mut self,
1755 peek_target: PeekTarget,
1756 literal_constraints: Option<Vec<Row>>,
1757 uuid: Uuid,
1758 timestamp: Timestamp,
1759 result_desc: RelationDesc,
1760 finishing: RowSetFinishing,
1761 map_filter_project: mz_expr::SafeMfpPlan,
1762 mut read_hold: ReadHold,
1763 target_replica: Option<ReplicaId>,
1764 peek_response_tx: oneshot::Sender<PeekResponse>,
1765 ) -> Result<(), PeekError> {
1766 use PeekError::*;
1767
1768 let target_id = peek_target.id();
1769
1770 if read_hold.id() != target_id {
1772 return Err(ReadHoldIdMismatch(read_hold.id()));
1773 }
1774 read_hold
1775 .try_downgrade(Antichain::from_elem(timestamp.clone()))
1776 .map_err(|_| ReadHoldInsufficient(target_id))?;
1777
1778 if let Some(target) = target_replica {
1779 if !self.replica_exists(target) {
1780 return Err(ReplicaMissing(target));
1781 }
1782 }
1783
1784 let otel_ctx = OpenTelemetryContext::obtain();
1785
1786 self.peeks.insert(
1787 uuid,
1788 PendingPeek {
1789 target_replica,
1790 otel_ctx: otel_ctx.clone(),
1792 requested_at: Instant::now(),
1793 read_hold,
1794 peek_response_tx,
1795 limit: finishing.limit.map(usize::cast_from),
1796 offset: finishing.offset,
1797 },
1798 );
1799
1800 let peek = Peek {
1801 literal_constraints,
1802 uuid,
1803 timestamp,
1804 finishing,
1805 map_filter_project,
1806 otel_ctx,
1809 target: peek_target,
1810 result_desc,
1811 };
1812 self.send(ComputeCommand::Peek(Box::new(peek)));
1813
1814 Ok(())
1815 }
1816
1817 #[mz_ore::instrument(level = "debug")]
1819 pub fn cancel_peek(&mut self, uuid: Uuid, reason: PeekResponse) {
1820 let Some(peek) = self.peeks.get_mut(&uuid) else {
1821 tracing::warn!("did not find pending peek for {uuid}");
1822 return;
1823 };
1824
1825 let duration = peek.requested_at.elapsed();
1826 self.metrics
1827 .observe_peek_response(&PeekResponse::Canceled, duration);
1828
1829 let otel_ctx = peek.otel_ctx.clone();
1831 otel_ctx.attach_as_parent();
1832
1833 self.deliver_response(ComputeControllerResponse::PeekNotification(
1834 uuid,
1835 PeekNotification::Canceled,
1836 otel_ctx,
1837 ));
1838
1839 self.finish_peek(uuid, reason);
1842 }
1843
1844 #[mz_ore::instrument(level = "debug")]
1856 pub fn set_read_policy(
1857 &mut self,
1858 policies: Vec<(GlobalId, ReadPolicy)>,
1859 ) -> Result<(), ReadPolicyError> {
1860 for (id, _policy) in &policies {
1863 let collection = self.collection(*id)?;
1864 if collection.read_policy.is_none() {
1865 return Err(ReadPolicyError::WriteOnlyCollection(*id));
1866 }
1867 }
1868
1869 for (id, new_policy) in policies {
1870 let collection = self.expect_collection_mut(id);
1871 let new_since = new_policy.frontier(collection.write_frontier().borrow());
1872 let _ = collection.implied_read_hold.try_downgrade(new_since);
1873 collection.read_policy = Some(new_policy);
1874 }
1875
1876 Ok(())
1877 }
1878
1879 #[mz_ore::instrument(level = "debug")]
1887 fn maybe_update_global_write_frontier(
1888 &mut self,
1889 id: GlobalId,
1890 new_frontier: Antichain<Timestamp>,
1891 ) {
1892 let collection = self.expect_collection_mut(id);
1893
1894 let advanced = collection.shared.lock_write_frontier(|f| {
1895 let advanced = PartialOrder::less_than(f, &new_frontier);
1896 if advanced {
1897 f.clone_from(&new_frontier);
1898 }
1899 advanced
1900 });
1901
1902 if !advanced {
1903 return;
1904 }
1905
1906 let new_since = match &collection.read_policy {
1908 Some(read_policy) => {
1909 read_policy.frontier(new_frontier.borrow())
1912 }
1913 None => {
1914 Antichain::from_iter(
1923 new_frontier
1924 .iter()
1925 .map(|t| t.step_back().unwrap_or(Timestamp::MIN)),
1926 )
1927 }
1928 };
1929 let _ = collection.implied_read_hold.try_downgrade(new_since);
1930
1931 self.deliver_response(ComputeControllerResponse::FrontierUpper {
1933 id,
1934 upper: new_frontier,
1935 });
1936 }
1937
1938 pub(super) fn apply_read_hold_change(
1940 &mut self,
1941 id: GlobalId,
1942 mut update: ChangeBatch<Timestamp>,
1943 ) {
1944 let Some(collection) = self.collections.get_mut(&id) else {
1945 soft_panic_or_log!(
1946 "read hold change for absent collection (id={id}, changes={update:?})"
1947 );
1948 return;
1949 };
1950
1951 let new_since = collection.shared.lock_read_capabilities(|caps| {
1952 let read_frontier = caps.frontier();
1955 for (time, diff) in update.iter() {
1956 let count = caps.count_for(time) + diff;
1957 assert!(
1958 count >= 0,
1959 "invalid read capabilities update: negative capability \
1960 (id={id:?}, read_capabilities={caps:?}, update={update:?})",
1961 );
1962 assert!(
1963 count == 0 || read_frontier.less_equal(time),
1964 "invalid read capabilities update: frontier regression \
1965 (id={id:?}, read_capabilities={caps:?}, update={update:?})",
1966 );
1967 }
1968
1969 let changes = caps.update_iter(update.drain());
1972
1973 let changed = changes.count() > 0;
1974 changed.then(|| caps.frontier().to_owned())
1975 });
1976
1977 let Some(new_since) = new_since else {
1978 return; };
1980
1981 for read_hold in collection.compute_dependencies.values_mut() {
1983 read_hold
1984 .try_downgrade(new_since.clone())
1985 .expect("frontiers don't regress");
1986 }
1987 for read_hold in collection.storage_dependencies.values_mut() {
1988 read_hold
1989 .try_downgrade(new_since.clone())
1990 .expect("frontiers don't regress");
1991 }
1992
1993 self.send(ComputeCommand::AllowCompaction {
1995 id,
1996 frontier: new_since,
1997 });
1998 }
1999
2000 fn finish_peek(&mut self, uuid: Uuid, response: PeekResponse) {
2009 let Some(peek) = self.peeks.remove(&uuid) else {
2010 return;
2011 };
2012
2013 let _ = peek.peek_response_tx.send(response);
2015
2016 self.send(ComputeCommand::CancelPeek { uuid });
2019
2020 drop(peek.read_hold);
2021 }
2022
2023 fn handle_response(&mut self, (replica_id, epoch, response): ReplicaResponse) {
2026 if self
2028 .replicas
2029 .get(&replica_id)
2030 .filter(|replica| replica.epoch == epoch)
2031 .is_none()
2032 {
2033 return;
2034 }
2035
2036 match response {
2039 ComputeResponse::Frontiers(id, frontiers) => {
2040 self.handle_frontiers_response(id, frontiers, replica_id);
2041 }
2042 ComputeResponse::PeekResponse(uuid, peek_response, otel_ctx) => {
2043 self.handle_peek_response(uuid, peek_response, otel_ctx, replica_id);
2044 }
2045 ComputeResponse::CopyToResponse(id, response) => {
2046 self.handle_copy_to_response(id, response, replica_id);
2047 }
2048 ComputeResponse::SubscribeResponse(id, response) => {
2049 self.handle_subscribe_response(id, response, replica_id);
2050 }
2051 ComputeResponse::Status(response) => {
2052 self.handle_status_response(response, replica_id);
2053 }
2054 }
2055 }
2056
2057 fn handle_frontiers_response(
2060 &mut self,
2061 id: GlobalId,
2062 frontiers: FrontiersResponse,
2063 replica_id: ReplicaId,
2064 ) {
2065 if !self.collections.contains_key(&id) {
2066 soft_panic_or_log!(
2067 "frontiers update for an unknown collection \
2068 (id={id}, replica_id={replica_id}, frontiers={frontiers:?})"
2069 );
2070 return;
2071 }
2072 let Some(replica) = self.replicas.get_mut(&replica_id) else {
2073 soft_panic_or_log!(
2074 "frontiers update for an unknown replica \
2075 (replica_id={replica_id}, frontiers={frontiers:?})"
2076 );
2077 return;
2078 };
2079 let Some(replica_collection) = replica.collections.get_mut(&id) else {
2080 soft_panic_or_log!(
2081 "frontiers update for an unknown replica collection \
2082 (id={id}, replica_id={replica_id}, frontiers={frontiers:?})"
2083 );
2084 return;
2085 };
2086
2087 if let Some(new_frontier) = frontiers.input_frontier {
2088 replica_collection.update_input_frontier(new_frontier.clone());
2089 }
2090 if let Some(new_frontier) = frontiers.output_frontier {
2091 replica_collection.update_output_frontier(new_frontier.clone());
2092 }
2093 if let Some(new_frontier) = frontiers.write_frontier {
2094 replica_collection.update_write_frontier(new_frontier.clone());
2095 self.maybe_update_global_write_frontier(id, new_frontier);
2096 }
2097 }
2098
2099 #[mz_ore::instrument(level = "debug")]
2100 fn handle_peek_response(
2101 &mut self,
2102 uuid: Uuid,
2103 response: PeekResponse,
2104 otel_ctx: OpenTelemetryContext,
2105 replica_id: ReplicaId,
2106 ) {
2107 otel_ctx.attach_as_parent();
2108
2109 let Some(peek) = self.peeks.get(&uuid) else {
2112 return;
2113 };
2114
2115 let target_replica = peek.target_replica.unwrap_or(replica_id);
2117 if target_replica != replica_id {
2118 return;
2119 }
2120
2121 let duration = peek.requested_at.elapsed();
2122 self.metrics.observe_peek_response(&response, duration);
2123
2124 let notification = PeekNotification::new(&response, peek.offset, peek.limit);
2125 self.deliver_response(ComputeControllerResponse::PeekNotification(
2128 uuid,
2129 notification,
2130 otel_ctx,
2131 ));
2132
2133 self.finish_peek(uuid, response)
2134 }
2135
2136 fn handle_copy_to_response(
2137 &mut self,
2138 sink_id: GlobalId,
2139 response: CopyToResponse,
2140 replica_id: ReplicaId,
2141 ) {
2142 if !self.collections.contains_key(&sink_id) {
2143 soft_panic_or_log!(
2144 "received response for an unknown copy-to \
2145 (sink_id={sink_id}, replica_id={replica_id})",
2146 );
2147 return;
2148 }
2149 let Some(replica) = self.replicas.get_mut(&replica_id) else {
2150 soft_panic_or_log!("copy-to response for an unknown replica (replica_id={replica_id})");
2151 return;
2152 };
2153 let Some(replica_collection) = replica.collections.get_mut(&sink_id) else {
2154 soft_panic_or_log!(
2155 "copy-to response for an unknown replica collection \
2156 (sink_id={sink_id}, replica_id={replica_id})"
2157 );
2158 return;
2159 };
2160
2161 replica_collection.update_write_frontier(Antichain::new());
2165 replica_collection.update_input_frontier(Antichain::new());
2166 replica_collection.update_output_frontier(Antichain::new());
2167
2168 if !self.copy_tos.remove(&sink_id) {
2171 return;
2172 }
2173
2174 let result = match response {
2175 CopyToResponse::RowCount(count) => Ok(count),
2176 CopyToResponse::Error(error) => Err(anyhow::anyhow!(error)),
2177 CopyToResponse::Dropped => {
2182 tracing::error!(
2183 %sink_id, %replica_id,
2184 "received `Dropped` response for a tracked copy to",
2185 );
2186 return;
2187 }
2188 };
2189
2190 self.deliver_response(ComputeControllerResponse::CopyToResponse(sink_id, result));
2191 }
2192
2193 fn handle_subscribe_response(
2194 &mut self,
2195 subscribe_id: GlobalId,
2196 response: SubscribeResponse,
2197 replica_id: ReplicaId,
2198 ) {
2199 if !self.collections.contains_key(&subscribe_id) {
2200 soft_panic_or_log!(
2201 "received response for an unknown subscribe \
2202 (subscribe_id={subscribe_id}, replica_id={replica_id})",
2203 );
2204 return;
2205 }
2206 let Some(replica) = self.replicas.get_mut(&replica_id) else {
2207 soft_panic_or_log!(
2208 "subscribe response for an unknown replica (replica_id={replica_id})"
2209 );
2210 return;
2211 };
2212 let Some(replica_collection) = replica.collections.get_mut(&subscribe_id) else {
2213 soft_panic_or_log!(
2214 "subscribe response for an unknown replica collection \
2215 (subscribe_id={subscribe_id}, replica_id={replica_id})"
2216 );
2217 return;
2218 };
2219
2220 let write_frontier = match &response {
2224 SubscribeResponse::Batch(batch) => batch.upper.clone(),
2225 SubscribeResponse::DroppedAt(_) => Antichain::new(),
2226 };
2227
2228 replica_collection.update_write_frontier(write_frontier.clone());
2232 replica_collection.update_input_frontier(write_frontier.clone());
2233 replica_collection.update_output_frontier(write_frontier.clone());
2234
2235 let Some(mut subscribe) = self.subscribes.get(&subscribe_id).cloned() else {
2237 return;
2238 };
2239
2240 self.maybe_update_global_write_frontier(subscribe_id, write_frontier);
2246
2247 match response {
2248 SubscribeResponse::Batch(batch) => {
2249 let upper = batch.upper;
2250 let mut updates = batch.updates;
2251
2252 if PartialOrder::less_than(&subscribe.frontier, &upper) {
2255 let lower = std::mem::replace(&mut subscribe.frontier, upper.clone());
2256
2257 if upper.is_empty() {
2258 self.subscribes.remove(&subscribe_id);
2260 } else {
2261 self.subscribes.insert(subscribe_id, subscribe);
2263 }
2264
2265 if let Ok(updates) = updates.as_mut() {
2266 updates.retain_mut(|updates| {
2267 let offset = updates.times().partition_point(|t| {
2268 !lower.less_equal(t)
2271 });
2272 let (_, past_lower) = std::mem::take(updates).split_at(offset);
2273 *updates = past_lower;
2274 updates.len() > 0
2275 });
2276 }
2277 self.deliver_response(ComputeControllerResponse::SubscribeResponse(
2278 subscribe_id,
2279 SubscribeBatch {
2280 lower,
2281 upper,
2282 updates,
2283 },
2284 ));
2285 }
2286 }
2287 SubscribeResponse::DroppedAt(frontier) => {
2288 tracing::error!(
2293 %subscribe_id,
2294 %replica_id,
2295 frontier = ?frontier.elements(),
2296 "received `DroppedAt` response for a tracked subscribe",
2297 );
2298 self.subscribes.remove(&subscribe_id);
2299 }
2300 }
2301 }
2302
2303 fn handle_status_response(&self, response: StatusResponse, _replica_id: ReplicaId) {
2304 match response {
2305 StatusResponse::Placeholder => {}
2306 }
2307 }
2308
2309 fn dependency_write_frontiers<'b>(
2311 &'b self,
2312 collection: &'b CollectionState,
2313 ) -> impl Iterator<Item = Antichain<Timestamp>> + 'b {
2314 let compute_frontiers = collection.compute_dependency_ids().filter_map(|dep_id| {
2315 let collection = self.collections.get(&dep_id);
2316 collection.map(|c| c.write_frontier())
2317 });
2318 let storage_frontiers = collection.storage_dependency_ids().filter_map(|dep_id| {
2319 let frontiers = self.storage_collections.collection_frontiers(dep_id).ok();
2320 frontiers.map(|f| f.write_frontier)
2321 });
2322
2323 compute_frontiers.chain(storage_frontiers)
2324 }
2325
2326 fn transitive_storage_dependency_write_frontiers<'b>(
2328 &'b self,
2329 collection: &'b CollectionState,
2330 ) -> impl Iterator<Item = Antichain<Timestamp>> + 'b {
2331 let mut storage_ids: BTreeSet<_> = collection.storage_dependency_ids().collect();
2332 let mut todo: Vec<_> = collection.compute_dependency_ids().collect();
2333 let mut done = BTreeSet::new();
2334
2335 while let Some(id) = todo.pop() {
2336 if done.contains(&id) {
2337 continue;
2338 }
2339 if let Some(dep) = self.collections.get(&id) {
2340 storage_ids.extend(dep.storage_dependency_ids());
2341 todo.extend(dep.compute_dependency_ids())
2342 }
2343 done.insert(id);
2344 }
2345
2346 let storage_frontiers = storage_ids.into_iter().filter_map(|id| {
2347 let frontiers = self.storage_collections.collection_frontiers(id).ok();
2348 frontiers.map(|f| f.write_frontier)
2349 });
2350
2351 storage_frontiers
2352 }
2353
2354 fn downgrade_warmup_capabilities(&mut self) {
2367 let mut new_capabilities = BTreeMap::new();
2368 for (id, collection) in &self.collections {
2369 if collection.read_policy.is_none()
2373 && collection.shared.lock_write_frontier(|f| f.is_empty())
2374 {
2375 new_capabilities.insert(*id, Antichain::new());
2376 continue;
2377 }
2378
2379 let mut new_capability = Antichain::new();
2380 for frontier in self.dependency_write_frontiers(collection) {
2381 for time in frontier {
2382 new_capability.insert(time.step_back().unwrap_or(time));
2383 }
2384 }
2385
2386 new_capabilities.insert(*id, new_capability);
2387 }
2388
2389 for (id, new_capability) in new_capabilities {
2390 let collection = self.expect_collection_mut(id);
2391 let _ = collection.warmup_read_hold.try_downgrade(new_capability);
2392 }
2393 }
2394
2395 fn forward_implied_capabilities(&mut self) {
2423 if !ENABLE_PAUSED_CLUSTER_READHOLD_DOWNGRADE.get(&self.dyncfg) {
2424 return;
2425 }
2426 if !self.replicas.is_empty() {
2427 return;
2428 }
2429
2430 let mut new_capabilities = BTreeMap::new();
2431 for (id, collection) in &self.collections {
2432 let Some(read_policy) = &collection.read_policy else {
2433 continue;
2435 };
2436
2437 let mut dep_frontier = Antichain::new();
2441 for frontier in self.transitive_storage_dependency_write_frontiers(collection) {
2442 dep_frontier.extend(frontier);
2443 }
2444
2445 let new_capability = read_policy.frontier(dep_frontier.borrow());
2446 if PartialOrder::less_than(collection.implied_read_hold.since(), &new_capability) {
2447 new_capabilities.insert(*id, new_capability);
2448 }
2449 }
2450
2451 for (id, new_capability) in new_capabilities {
2452 let collection = self.expect_collection_mut(id);
2453 let _ = collection.implied_read_hold.try_downgrade(new_capability);
2454 }
2455 }
2456
2457 pub(super) fn acquire_read_hold(&self, id: GlobalId) -> Result<ReadHold, CollectionMissing> {
2462 let collection = self.collection(id)?;
2468 let since = collection.shared.lock_read_capabilities(|caps| {
2469 let since = caps.frontier().to_owned();
2470 caps.update_iter(since.iter().map(|t| (t.clone(), 1)));
2471 since
2472 });
2473 let hold = ReadHold::new(id, since, Arc::clone(&self.read_hold_tx));
2474 Ok(hold)
2475 }
2476
2477 #[mz_ore::instrument(level = "debug")]
2483 pub fn maintain(&mut self) {
2484 self.rehydrate_failed_replicas();
2485 self.downgrade_warmup_capabilities();
2486 self.forward_implied_capabilities();
2487 self.schedule_collections();
2488 self.cleanup_collections();
2489 self.update_frontier_introspection();
2490 self.refresh_state_metrics();
2491 self.refresh_wallclock_lag();
2492 }
2493}
2494
2495#[derive(Debug)]
2500struct CollectionState {
2501 target_replica: Option<ReplicaId>,
2503 log_collection: bool,
2507 dropped: bool,
2513 scheduled: bool,
2516
2517 read_only: bool,
2521
2522 shared: SharedCollectionState,
2524
2525 implied_read_hold: ReadHold,
2532 warmup_read_hold: ReadHold,
2540 read_policy: Option<ReadPolicy>,
2546
2547 storage_dependencies: BTreeMap<GlobalId, ReadHold>,
2550 compute_dependencies: BTreeMap<GlobalId, ReadHold>,
2553
2554 introspection: CollectionIntrospection,
2556
2557 wallclock_lag_histogram_stash: Option<
2564 BTreeMap<
2565 (
2566 WallclockLagHistogramPeriod,
2567 WallclockLag,
2568 BTreeMap<&'static str, String>,
2569 ),
2570 Diff,
2571 >,
2572 >,
2573}
2574
2575impl CollectionState {
2576 fn new(
2578 collection_id: GlobalId,
2579 as_of: Antichain<Timestamp>,
2580 shared: SharedCollectionState,
2581 storage_dependencies: BTreeMap<GlobalId, ReadHold>,
2582 compute_dependencies: BTreeMap<GlobalId, ReadHold>,
2583 read_hold_tx: read_holds::ChangeTx,
2584 introspection: CollectionIntrospection,
2585 ) -> Self {
2586 let since = as_of.clone();
2588 let upper = as_of;
2590
2591 assert!(shared.lock_read_capabilities(|c| c.frontier() == since.borrow()));
2593 assert!(shared.lock_write_frontier(|f| f == &upper));
2594
2595 let implied_read_hold =
2599 ReadHold::new(collection_id, since.clone(), Arc::clone(&read_hold_tx));
2600 let warmup_read_hold = ReadHold::new(collection_id, since.clone(), read_hold_tx);
2601
2602 let updates = warmup_read_hold.since().iter().map(|t| (t.clone(), 1));
2603 shared.lock_read_capabilities(|c| {
2604 c.update_iter(updates);
2605 });
2606
2607 let wallclock_lag_histogram_stash = match collection_id.is_transient() {
2611 true => None,
2612 false => Some(Default::default()),
2613 };
2614
2615 Self {
2616 target_replica: None,
2617 log_collection: false,
2618 dropped: false,
2619 scheduled: false,
2620 read_only: true,
2621 shared,
2622 implied_read_hold,
2623 warmup_read_hold,
2624 read_policy: Some(ReadPolicy::ValidFrom(since)),
2625 storage_dependencies,
2626 compute_dependencies,
2627 introspection,
2628 wallclock_lag_histogram_stash,
2629 }
2630 }
2631
2632 fn new_log_collection(
2634 id: GlobalId,
2635 shared: SharedCollectionState,
2636 read_hold_tx: read_holds::ChangeTx,
2637 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
2638 ) -> Self {
2639 let since = Antichain::from_elem(Timestamp::MIN);
2640 let introspection = CollectionIntrospection::new(
2641 id,
2642 introspection_tx,
2643 since.clone(),
2644 false,
2645 None,
2646 None,
2647 Vec::new(),
2648 );
2649 let mut state = Self::new(
2650 id,
2651 since,
2652 shared,
2653 Default::default(),
2654 Default::default(),
2655 read_hold_tx,
2656 introspection,
2657 );
2658 state.log_collection = true;
2659 state.scheduled = true;
2661 state
2662 }
2663
2664 fn read_frontier(&self) -> Antichain<Timestamp> {
2666 self.shared
2667 .lock_read_capabilities(|c| c.frontier().to_owned())
2668 }
2669
2670 fn write_frontier(&self) -> Antichain<Timestamp> {
2672 self.shared.lock_write_frontier(|f| f.clone())
2673 }
2674
2675 fn storage_dependency_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
2676 self.storage_dependencies.keys().copied()
2677 }
2678
2679 fn compute_dependency_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
2680 self.compute_dependencies.keys().copied()
2681 }
2682}
2683
2684#[derive(Clone, Debug)]
2695pub(super) struct SharedCollectionState {
2696 read_capabilities: Arc<Mutex<MutableAntichain<Timestamp>>>,
2709 write_frontier: Arc<Mutex<Antichain<Timestamp>>>,
2711}
2712
2713impl SharedCollectionState {
2714 pub fn new(as_of: Antichain<Timestamp>) -> Self {
2715 let since = as_of.clone();
2717 let upper = as_of;
2719
2720 let mut read_capabilities = MutableAntichain::new();
2724 read_capabilities.update_iter(since.iter().map(|time| (time.clone(), 1)));
2725
2726 Self {
2727 read_capabilities: Arc::new(Mutex::new(read_capabilities)),
2728 write_frontier: Arc::new(Mutex::new(upper)),
2729 }
2730 }
2731
2732 pub fn lock_read_capabilities<F, R>(&self, f: F) -> R
2733 where
2734 F: FnOnce(&mut MutableAntichain<Timestamp>) -> R,
2735 {
2736 let mut caps = self.read_capabilities.lock().expect("poisoned");
2737 f(&mut *caps)
2738 }
2739
2740 pub fn lock_write_frontier<F, R>(&self, f: F) -> R
2741 where
2742 F: FnOnce(&mut Antichain<Timestamp>) -> R,
2743 {
2744 let mut frontier = self.write_frontier.lock().expect("poisoned");
2745 f(&mut *frontier)
2746 }
2747}
2748
2749#[derive(Debug)]
2752struct CollectionIntrospection {
2753 collection_id: GlobalId,
2755 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
2757 frontiers: Option<FrontiersIntrospectionState>,
2762 refresh: Option<RefreshIntrospectionState>,
2766 dependency_ids: Vec<GlobalId>,
2768}
2769
2770impl CollectionIntrospection {
2771 fn new(
2772 collection_id: GlobalId,
2773 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
2774 as_of: Antichain<Timestamp>,
2775 storage_sink: bool,
2776 initial_as_of: Option<Antichain<Timestamp>>,
2777 refresh_schedule: Option<RefreshSchedule>,
2778 dependency_ids: Vec<GlobalId>,
2779 ) -> Self {
2780 let refresh =
2781 match (refresh_schedule, initial_as_of) {
2782 (Some(refresh_schedule), Some(initial_as_of)) => Some(
2783 RefreshIntrospectionState::new(refresh_schedule, initial_as_of, &as_of),
2784 ),
2785 (refresh_schedule, _) => {
2786 soft_assert_or_log!(
2789 refresh_schedule.is_none(),
2790 "`refresh_schedule` without an `initial_as_of`: {collection_id}"
2791 );
2792 None
2793 }
2794 };
2795 let frontiers = (!storage_sink).then(|| FrontiersIntrospectionState::new(as_of));
2796
2797 let self_ = Self {
2798 collection_id,
2799 introspection_tx,
2800 frontiers,
2801 refresh,
2802 dependency_ids,
2803 };
2804
2805 self_.report_initial_state();
2806 self_
2807 }
2808
2809 fn report_initial_state(&self) {
2811 if let Some(frontiers) = &self.frontiers {
2812 let row = frontiers.row_for_collection(self.collection_id);
2813 let updates = vec![(row, Diff::ONE)];
2814 self.send(IntrospectionType::Frontiers, updates);
2815 }
2816
2817 if let Some(refresh) = &self.refresh {
2818 let row = refresh.row_for_collection(self.collection_id);
2819 let updates = vec![(row, Diff::ONE)];
2820 self.send(IntrospectionType::ComputeMaterializedViewRefreshes, updates);
2821 }
2822
2823 if !self.dependency_ids.is_empty() {
2824 let updates = self.dependency_rows(Diff::ONE);
2825 self.send(IntrospectionType::ComputeDependencies, updates);
2826 }
2827 }
2828
2829 fn dependency_rows(&self, diff: Diff) -> Vec<(Row, Diff)> {
2831 self.dependency_ids
2832 .iter()
2833 .map(|dependency_id| {
2834 let row = Row::pack_slice(&[
2835 Datum::String(&self.collection_id.to_string()),
2836 Datum::String(&dependency_id.to_string()),
2837 ]);
2838 (row, diff)
2839 })
2840 .collect()
2841 }
2842
2843 fn observe_frontiers(
2846 &mut self,
2847 read_frontier: &Antichain<Timestamp>,
2848 write_frontier: &Antichain<Timestamp>,
2849 ) {
2850 self.update_frontier_introspection(read_frontier, write_frontier);
2851 self.update_refresh_introspection(write_frontier);
2852 }
2853
2854 fn update_frontier_introspection(
2855 &mut self,
2856 read_frontier: &Antichain<Timestamp>,
2857 write_frontier: &Antichain<Timestamp>,
2858 ) {
2859 let Some(frontiers) = &mut self.frontiers else {
2860 return;
2861 };
2862
2863 if &frontiers.read_frontier == read_frontier && &frontiers.write_frontier == write_frontier
2864 {
2865 return; };
2867
2868 let retraction = frontiers.row_for_collection(self.collection_id);
2869 frontiers.update(read_frontier, write_frontier);
2870 let insertion = frontiers.row_for_collection(self.collection_id);
2871 let updates = vec![(retraction, Diff::MINUS_ONE), (insertion, Diff::ONE)];
2872 self.send(IntrospectionType::Frontiers, updates);
2873 }
2874
2875 fn update_refresh_introspection(&mut self, write_frontier: &Antichain<Timestamp>) {
2876 let Some(refresh) = &mut self.refresh else {
2877 return;
2878 };
2879
2880 let retraction = refresh.row_for_collection(self.collection_id);
2881 refresh.frontier_update(write_frontier);
2882 let insertion = refresh.row_for_collection(self.collection_id);
2883
2884 if retraction == insertion {
2885 return; }
2887
2888 let updates = vec![(retraction, Diff::MINUS_ONE), (insertion, Diff::ONE)];
2889 self.send(IntrospectionType::ComputeMaterializedViewRefreshes, updates);
2890 }
2891
2892 fn send(&self, introspection_type: IntrospectionType, updates: Vec<(Row, Diff)>) {
2893 let _ = self.introspection_tx.send((introspection_type, updates));
2896 }
2897}
2898
2899impl Drop for CollectionIntrospection {
2900 fn drop(&mut self) {
2901 if let Some(frontiers) = &self.frontiers {
2903 let row = frontiers.row_for_collection(self.collection_id);
2904 let updates = vec![(row, Diff::MINUS_ONE)];
2905 self.send(IntrospectionType::Frontiers, updates);
2906 }
2907
2908 if let Some(refresh) = &self.refresh {
2910 let retraction = refresh.row_for_collection(self.collection_id);
2911 let updates = vec![(retraction, Diff::MINUS_ONE)];
2912 self.send(IntrospectionType::ComputeMaterializedViewRefreshes, updates);
2913 }
2914
2915 if !self.dependency_ids.is_empty() {
2917 let updates = self.dependency_rows(Diff::MINUS_ONE);
2918 self.send(IntrospectionType::ComputeDependencies, updates);
2919 }
2920 }
2921}
2922
2923#[derive(Debug)]
2924struct FrontiersIntrospectionState {
2925 read_frontier: Antichain<Timestamp>,
2926 write_frontier: Antichain<Timestamp>,
2927}
2928
2929impl FrontiersIntrospectionState {
2930 fn new(as_of: Antichain<Timestamp>) -> Self {
2931 Self {
2932 read_frontier: as_of.clone(),
2933 write_frontier: as_of,
2934 }
2935 }
2936
2937 fn row_for_collection(&self, collection_id: GlobalId) -> Row {
2939 let read_frontier = self
2940 .read_frontier
2941 .as_option()
2942 .map_or(Datum::Null, |ts| ts.clone().into());
2943 let write_frontier = self
2944 .write_frontier
2945 .as_option()
2946 .map_or(Datum::Null, |ts| ts.clone().into());
2947 Row::pack_slice(&[
2948 Datum::String(&collection_id.to_string()),
2949 read_frontier,
2950 write_frontier,
2951 ])
2952 }
2953
2954 fn update(
2956 &mut self,
2957 read_frontier: &Antichain<Timestamp>,
2958 write_frontier: &Antichain<Timestamp>,
2959 ) {
2960 if read_frontier != &self.read_frontier {
2961 self.read_frontier.clone_from(read_frontier);
2962 }
2963 if write_frontier != &self.write_frontier {
2964 self.write_frontier.clone_from(write_frontier);
2965 }
2966 }
2967}
2968
2969#[derive(Debug)]
2972struct RefreshIntrospectionState {
2973 refresh_schedule: RefreshSchedule,
2975 initial_as_of: Antichain<Timestamp>,
2976 next_refresh: Datum<'static>, last_completed_refresh: Datum<'static>, }
2980
2981impl RefreshIntrospectionState {
2982 fn row_for_collection(&self, collection_id: GlobalId) -> Row {
2984 Row::pack_slice(&[
2985 Datum::String(&collection_id.to_string()),
2986 self.last_completed_refresh,
2987 self.next_refresh,
2988 ])
2989 }
2990}
2991
2992impl RefreshIntrospectionState {
2993 fn new(
2996 refresh_schedule: RefreshSchedule,
2997 initial_as_of: Antichain<Timestamp>,
2998 upper: &Antichain<Timestamp>,
2999 ) -> Self {
3000 let mut self_ = Self {
3001 refresh_schedule: refresh_schedule.clone(),
3002 initial_as_of: initial_as_of.clone(),
3003 next_refresh: Datum::Null,
3004 last_completed_refresh: Datum::Null,
3005 };
3006 self_.frontier_update(upper);
3007 self_
3008 }
3009
3010 fn frontier_update(&mut self, write_frontier: &Antichain<Timestamp>) {
3013 if write_frontier.is_empty() {
3014 self.last_completed_refresh =
3015 if let Some(last_refresh) = self.refresh_schedule.last_refresh() {
3016 last_refresh.into()
3017 } else {
3018 Timestamp::MAX.into()
3021 };
3022 self.next_refresh = Datum::Null;
3023 } else {
3024 if PartialOrder::less_equal(write_frontier, &self.initial_as_of) {
3025 self.last_completed_refresh = Datum::Null;
3027 let initial_as_of = self.initial_as_of.as_option().expect(
3028 "initial_as_of can't be [], because then there would be no refreshes at all",
3029 );
3030 let first_refresh = self
3031 .refresh_schedule
3032 .round_up_timestamp(*initial_as_of)
3033 .expect("sequencing makes sure that REFRESH MVs always have a first refresh");
3034 soft_assert_or_log!(
3035 first_refresh == *initial_as_of,
3036 "initial_as_of should be set to the first refresh"
3037 );
3038 self.next_refresh = first_refresh.into();
3039 } else {
3040 let write_frontier = write_frontier.as_option().expect("checked above");
3042 self.last_completed_refresh = self
3043 .refresh_schedule
3044 .round_down_timestamp_m1(*write_frontier)
3045 .map_or_else(
3046 || {
3047 soft_panic_or_log!(
3048 "rounding down should have returned the first refresh or later"
3049 );
3050 Datum::Null
3051 },
3052 |last_completed_refresh| last_completed_refresh.into(),
3053 );
3054 self.next_refresh = write_frontier.clone().into();
3055 }
3056 }
3057 }
3058}
3059
3060#[derive(Debug)]
3062struct PendingPeek {
3063 target_replica: Option<ReplicaId>,
3067 otel_ctx: OpenTelemetryContext,
3069 requested_at: Instant,
3073 read_hold: ReadHold,
3075 peek_response_tx: oneshot::Sender<PeekResponse>,
3077 limit: Option<usize>,
3079 offset: usize,
3081}
3082
3083#[derive(Debug, Clone)]
3084struct ActiveSubscribe {
3085 frontier: Antichain<Timestamp>,
3087}
3088
3089impl Default for ActiveSubscribe {
3090 fn default() -> Self {
3091 Self {
3092 frontier: Antichain::from_elem(Timestamp::MIN),
3093 }
3094 }
3095}
3096
3097#[derive(Debug)]
3099struct ReplicaState {
3100 id: ReplicaId,
3102 client: ReplicaClient,
3104 config: ReplicaConfig,
3106 metrics: ReplicaMetrics,
3108 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3110 collections: BTreeMap<GlobalId, ReplicaCollectionState>,
3112 epoch: u64,
3114}
3115
3116impl ReplicaState {
3117 fn new(
3118 id: ReplicaId,
3119 client: ReplicaClient,
3120 config: ReplicaConfig,
3121 metrics: ReplicaMetrics,
3122 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3123 epoch: u64,
3124 ) -> Self {
3125 Self {
3126 id,
3127 client,
3128 config,
3129 metrics,
3130 introspection_tx,
3131 epoch,
3132 collections: Default::default(),
3133 }
3134 }
3135
3136 fn add_collection(
3142 &mut self,
3143 id: GlobalId,
3144 as_of: Antichain<Timestamp>,
3145 input_read_holds: Vec<ReadHold>,
3146 ) {
3147 let metrics = self.metrics.for_collection(id);
3148 let introspection = ReplicaCollectionIntrospection::new(
3149 self.id,
3150 id,
3151 self.introspection_tx.clone(),
3152 as_of.clone(),
3153 );
3154 let mut state =
3155 ReplicaCollectionState::new(metrics, as_of, introspection, input_read_holds);
3156
3157 if id.is_transient() {
3161 state.wallclock_lag_max = None;
3162 }
3163
3164 if let Some(previous) = self.collections.insert(id, state) {
3165 panic!("attempt to add a collection with existing ID {id} (previous={previous:?}");
3166 }
3167 }
3168
3169 fn remove_collection(&mut self, id: GlobalId) -> Option<ReplicaCollectionState> {
3171 self.collections.remove(&id)
3172 }
3173
3174 fn collection_frontiers_empty(&self, id: GlobalId) -> bool {
3176 self.collections.get(&id).map_or(true, |c| {
3177 c.write_frontier.is_empty()
3178 && c.input_frontier.is_empty()
3179 && c.output_frontier.is_empty()
3180 })
3181 }
3182
3183 #[mz_ore::instrument(level = "debug")]
3187 pub fn dump(&self) -> Result<serde_json::Value, anyhow::Error> {
3188 let Self {
3195 id,
3196 client: _,
3197 config: _,
3198 metrics: _,
3199 introspection_tx: _,
3200 epoch,
3201 collections,
3202 } = self;
3203
3204 let collections: BTreeMap<_, _> = collections
3205 .iter()
3206 .map(|(id, collection)| (id.to_string(), format!("{collection:?}")))
3207 .collect();
3208
3209 Ok(serde_json::json!({
3210 "id": id.to_string(),
3211 "collections": collections,
3212 "epoch": epoch,
3213 }))
3214 }
3215}
3216
3217#[derive(Debug)]
3218struct ReplicaCollectionState {
3219 write_frontier: Antichain<Timestamp>,
3223 input_frontier: Antichain<Timestamp>,
3227 output_frontier: Antichain<Timestamp>,
3231
3232 metrics: Option<ReplicaCollectionMetrics>,
3236 as_of: Antichain<Timestamp>,
3238 introspection: ReplicaCollectionIntrospection,
3240 input_read_holds: Vec<ReadHold>,
3246
3247 wallclock_lag_max: Option<WallclockLag>,
3251}
3252
3253impl ReplicaCollectionState {
3254 fn new(
3255 metrics: Option<ReplicaCollectionMetrics>,
3256 as_of: Antichain<Timestamp>,
3257 introspection: ReplicaCollectionIntrospection,
3258 input_read_holds: Vec<ReadHold>,
3259 ) -> Self {
3260 Self {
3261 write_frontier: as_of.clone(),
3262 input_frontier: as_of.clone(),
3263 output_frontier: as_of.clone(),
3264 metrics,
3265 as_of,
3266 introspection,
3267 input_read_holds,
3268 wallclock_lag_max: Some(WallclockLag::MIN),
3269 }
3270 }
3271
3272 fn hydrated(&self) -> bool {
3274 self.as_of.is_empty() || PartialOrder::less_than(&self.as_of, &self.output_frontier)
3290 }
3291
3292 fn update_write_frontier(&mut self, new_frontier: Antichain<Timestamp>) {
3294 if PartialOrder::less_than(&new_frontier, &self.write_frontier) {
3295 soft_panic_or_log!(
3296 "replica collection write frontier regression (old={:?}, new={new_frontier:?})",
3297 self.write_frontier,
3298 );
3299 return;
3300 } else if new_frontier == self.write_frontier {
3301 return;
3302 }
3303
3304 self.write_frontier = new_frontier;
3305 }
3306
3307 fn update_input_frontier(&mut self, new_frontier: Antichain<Timestamp>) {
3309 if PartialOrder::less_than(&new_frontier, &self.input_frontier) {
3310 soft_panic_or_log!(
3311 "replica collection input frontier regression (old={:?}, new={new_frontier:?})",
3312 self.input_frontier,
3313 );
3314 return;
3315 } else if new_frontier == self.input_frontier {
3316 return;
3317 }
3318
3319 self.input_frontier = new_frontier;
3320
3321 for read_hold in &mut self.input_read_holds {
3323 let result = read_hold.try_downgrade(self.input_frontier.clone());
3324 soft_assert_or_log!(
3325 result.is_ok(),
3326 "read hold downgrade failed (read_hold={read_hold:?}, new_since={:?})",
3327 self.input_frontier,
3328 );
3329 }
3330 }
3331
3332 fn update_output_frontier(&mut self, new_frontier: Antichain<Timestamp>) {
3334 if PartialOrder::less_than(&new_frontier, &self.output_frontier) {
3335 soft_panic_or_log!(
3336 "replica collection output frontier regression (old={:?}, new={new_frontier:?})",
3337 self.output_frontier,
3338 );
3339 return;
3340 } else if new_frontier == self.output_frontier {
3341 return;
3342 }
3343
3344 self.output_frontier = new_frontier;
3345 }
3346}
3347
3348#[derive(Debug)]
3351struct ReplicaCollectionIntrospection {
3352 replica_id: ReplicaId,
3354 collection_id: GlobalId,
3356 write_frontier: Antichain<Timestamp>,
3358 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3360}
3361
3362impl ReplicaCollectionIntrospection {
3363 fn new(
3365 replica_id: ReplicaId,
3366 collection_id: GlobalId,
3367 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3368 as_of: Antichain<Timestamp>,
3369 ) -> Self {
3370 let self_ = Self {
3371 replica_id,
3372 collection_id,
3373 write_frontier: as_of,
3374 introspection_tx,
3375 };
3376
3377 self_.report_initial_state();
3378 self_
3379 }
3380
3381 fn report_initial_state(&self) {
3383 let row = self.write_frontier_row();
3384 let updates = vec![(row, Diff::ONE)];
3385 self.send(IntrospectionType::ReplicaFrontiers, updates);
3386 }
3387
3388 fn observe_frontier(&mut self, write_frontier: &Antichain<Timestamp>) {
3390 if self.write_frontier == *write_frontier {
3391 return; }
3393
3394 let retraction = self.write_frontier_row();
3395 self.write_frontier.clone_from(write_frontier);
3396 let insertion = self.write_frontier_row();
3397
3398 let updates = vec![(retraction, Diff::MINUS_ONE), (insertion, Diff::ONE)];
3399 self.send(IntrospectionType::ReplicaFrontiers, updates);
3400 }
3401
3402 fn write_frontier_row(&self) -> Row {
3404 let write_frontier = self
3405 .write_frontier
3406 .as_option()
3407 .map_or(Datum::Null, |ts| ts.clone().into());
3408 Row::pack_slice(&[
3409 Datum::String(&self.collection_id.to_string()),
3410 Datum::String(&self.replica_id.to_string()),
3411 write_frontier,
3412 ])
3413 }
3414
3415 fn send(&self, introspection_type: IntrospectionType, updates: Vec<(Row, Diff)>) {
3416 let _ = self.introspection_tx.send((introspection_type, updates));
3419 }
3420}
3421
3422impl Drop for ReplicaCollectionIntrospection {
3423 fn drop(&mut self) {
3424 let row = self.write_frontier_row();
3426 let updates = vec![(row, Diff::MINUS_ONE)];
3427 self.send(IntrospectionType::ReplicaFrontiers, updates);
3428 }
3429}
3430
3431#[cfg(test)]
3432mod tests {
3433 use std::collections::BTreeMap;
3434
3435 use mz_compute_types::dyncfgs::{ENABLE_COLUMN_PAGED_BATCHER, ENABLE_MZ_JOIN_CORE};
3436 use mz_dyncfg::{ConfigSet, ConfigUpdates, ConfigVal};
3437 use mz_persist_types::PersistLocation;
3438
3439 use crate::protocol::command::{ComputeCommand, InstanceConfig};
3440
3441 use super::{Instance, ReplicaId};
3442
3443 fn create_instance_command() -> ComputeCommand {
3444 ComputeCommand::CreateInstance(Box::new(InstanceConfig {
3445 logging: Default::default(),
3446 expiration_offset: None,
3447 peek_stash_persist_location: PersistLocation::new_in_mem(),
3448 arrangement_dictionary_compression: false,
3449 initial_config: Default::default(),
3450 }))
3451 }
3452
3453 fn initial_config(cmd: &ComputeCommand) -> &ConfigUpdates {
3454 match cmd {
3455 ComputeCommand::CreateInstance(config) => &config.initial_config,
3456 other => panic!("expected CreateInstance, got {other:?}"),
3457 }
3458 }
3459
3460 #[mz_ore::test]
3465 fn create_instance_snapshots_instance_wide_dyncfg() {
3466 let dyncfg = ConfigSet::default()
3467 .add(&ENABLE_COLUMN_PAGED_BATCHER)
3468 .add(&ENABLE_MZ_JOIN_CORE);
3469 let mut updates = ConfigUpdates::default();
3470 updates.add(&ENABLE_COLUMN_PAGED_BATCHER, true);
3471 updates.add(&ENABLE_MZ_JOIN_CORE, false);
3472 updates.apply(&dyncfg);
3473
3474 let overrides = BTreeMap::new();
3476 let cmd = Instance::specialize_command_for_replica(
3477 create_instance_command(),
3478 ReplicaId::User(1),
3479 &overrides,
3480 &dyncfg,
3481 );
3482 let snapshot = initial_config(&cmd);
3483 assert_eq!(
3484 snapshot.updates.get(ENABLE_COLUMN_PAGED_BATCHER.name()),
3485 Some(&ConfigVal::Bool(true)),
3486 );
3487 assert_eq!(
3488 snapshot.updates.get(ENABLE_MZ_JOIN_CORE.name()),
3489 Some(&ConfigVal::Bool(false)),
3490 );
3491 }
3492
3493 #[mz_ore::test]
3496 fn create_instance_snapshot_applies_replica_override() {
3497 let dyncfg = ConfigSet::default().add(&ENABLE_COLUMN_PAGED_BATCHER);
3498 let mut updates = ConfigUpdates::default();
3499 updates.add(&ENABLE_COLUMN_PAGED_BATCHER, true);
3500 updates.apply(&dyncfg);
3501
3502 let replica = ReplicaId::User(1);
3503 let mut override_updates = ConfigUpdates::default();
3504 override_updates.add(&ENABLE_COLUMN_PAGED_BATCHER, false);
3505 let overrides = BTreeMap::from([(replica, override_updates)]);
3506
3507 let cmd = Instance::specialize_command_for_replica(
3508 create_instance_command(),
3509 replica,
3510 &overrides,
3511 &dyncfg,
3512 );
3513 assert_eq!(
3514 initial_config(&cmd)
3515 .updates
3516 .get(ENABLE_COLUMN_PAGED_BATCHER.name()),
3517 Some(&ConfigVal::Bool(false)),
3518 "replica override should win over the instance-wide value",
3519 );
3520 }
3521
3522 #[mz_ore::test]
3524 fn update_configuration_merges_replica_override() {
3525 let dyncfg = ConfigSet::default().add(&ENABLE_COLUMN_PAGED_BATCHER);
3526
3527 let replica = ReplicaId::User(1);
3528 let mut override_updates = ConfigUpdates::default();
3529 override_updates.add(&ENABLE_COLUMN_PAGED_BATCHER, true);
3530 let overrides = BTreeMap::from([(replica, override_updates)]);
3531
3532 let cmd = Instance::specialize_command_for_replica(
3533 ComputeCommand::UpdateConfiguration(Box::new(Default::default())),
3534 replica,
3535 &overrides,
3536 &dyncfg,
3537 );
3538 match cmd {
3539 ComputeCommand::UpdateConfiguration(params) => assert_eq!(
3540 params
3541 .dyncfg_updates
3542 .updates
3543 .get(ENABLE_COLUMN_PAGED_BATCHER.name()),
3544 Some(&ConfigVal::Bool(true)),
3545 ),
3546 other => panic!("expected UpdateConfiguration, got {other:?}"),
3547 }
3548 }
3549}