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 differential_dataflow::lattice::Lattice;
19use mz_build_info::BuildInfo;
20use mz_cluster_client::WallclockLagFn;
21use mz_compute_types::dataflows::{BuildDesc, DataflowDescription, LirDataflowDescription};
22use mz_compute_types::plan::render_plan::RenderPlan;
23use mz_compute_types::sinks::{
24 ComputeSinkConnection, ComputeSinkDesc, MaterializedViewSinkConnection,
25};
26use mz_compute_types::sources::SourceInstanceDesc;
27use mz_controller_types::dyncfgs::{
28 ENABLE_PAUSED_CLUSTER_READHOLD_DOWNGRADE, WALLCLOCK_LAG_RECORDING_INTERVAL,
29};
30use mz_dyncfg::{ConfigSet, ConfigUpdates};
31use mz_expr::RowSetFinishing;
32use mz_ore::cast::CastFrom;
33use mz_ore::channel::instrumented_unbounded_channel;
34use mz_ore::now::NowFn;
35use mz_ore::tracing::OpenTelemetryContext;
36use mz_ore::{soft_assert_or_log, soft_panic_or_log};
37use mz_persist_types::PersistLocation;
38use mz_repr::adt::timestamp::CheckedTimestamp;
39use mz_repr::refresh_schedule::RefreshSchedule;
40use mz_repr::{Datum, Diff, GlobalId, RelationDesc, Row, Timestamp};
41use mz_storage_client::controller::{IntrospectionType, WallclockLag, WallclockLagHistogramPeriod};
42use mz_storage_types::read_holds::{self, ReadHold};
43use mz_storage_types::read_policy::ReadPolicy;
44use thiserror::Error;
45use timely::PartialOrder;
46use timely::progress::frontier::MutableAntichain;
47use timely::progress::{Antichain, ChangeBatch};
48use tokio::sync::{mpsc, oneshot};
49use uuid::Uuid;
50
51use crate::controller::error::{
52 CollectionMissing, ERROR_TARGET_REPLICA_FAILED, HydrationCheckBadTarget,
53};
54use crate::controller::instance_client::PeekError;
55use crate::controller::replica::{ReplicaClient, ReplicaConfig};
56use crate::controller::{
57 CollectionReadiness, ComputeControllerResponse, IntrospectionUpdates, PeekNotification,
58 ReplicaId, StorageCollections,
59};
60use crate::logging::LogVariant;
61use crate::metrics::IntCounter;
62use crate::metrics::{InstanceMetrics, ReplicaCollectionMetrics, ReplicaMetrics, UIntGauge};
63use crate::protocol::command::{
64 ComputeCommand, ComputeParameters, InstanceConfig, Peek, PeekTarget,
65};
66use crate::protocol::history::ComputeCommandHistory;
67use crate::protocol::response::{
68 ComputeResponse, CopyToResponse, FrontiersResponse, PeekError as ProtocolPeekError,
69 PeekResponse, StatusResponse, SubscribeBatch, SubscribeResponse,
70};
71
72#[derive(Error, Debug)]
73#[error("replica exists already: {0}")]
74pub(super) struct ReplicaExists(pub ReplicaId);
75
76#[derive(Error, Debug)]
77#[error("replica does not exist: {0}")]
78pub(super) struct ReplicaMissing(pub ReplicaId);
79
80#[derive(Error, Debug)]
81pub(super) enum DataflowCreationError {
82 #[error("collection does not exist: {0}")]
83 CollectionMissing(GlobalId),
84 #[error("replica does not exist: {0}")]
85 ReplicaMissing(ReplicaId),
86 #[error("dataflow definition lacks an as_of value")]
87 MissingAsOf,
88 #[error("subscribe dataflow has an empty as_of")]
89 EmptyAsOfForSubscribe,
90 #[error("copy to dataflow has an empty as_of")]
91 EmptyAsOfForCopyTo,
92 #[error("no read hold provided for dataflow import: {0}")]
93 ReadHoldMissing(GlobalId),
94 #[error("insufficient read hold provided for dataflow import: {0}")]
95 ReadHoldInsufficient(GlobalId),
96}
97
98impl From<CollectionMissing> for DataflowCreationError {
99 fn from(error: CollectionMissing) -> Self {
100 Self::CollectionMissing(error.0)
101 }
102}
103
104#[derive(Error, Debug)]
105pub(super) enum ReadPolicyError {
106 #[error("collection does not exist: {0}")]
107 CollectionMissing(GlobalId),
108 #[error("collection is write-only: {0}")]
109 WriteOnlyCollection(GlobalId),
110}
111
112impl From<CollectionMissing> for ReadPolicyError {
113 fn from(error: CollectionMissing) -> Self {
114 Self::CollectionMissing(error.0)
115 }
116}
117
118pub(super) type Command = Box<dyn FnOnce(&mut Instance) + Send>;
120
121pub(super) type ReplicaResponse = (ReplicaId, u64, ComputeResponse);
124
125pub(super) struct Instance {
127 build_info: &'static BuildInfo,
129 storage_collections: StorageCollections,
131 initialized: bool,
133 read_only: bool,
138 workload_class: Option<String>,
142 replicas: BTreeMap<ReplicaId, ReplicaState>,
144 collections: BTreeMap<GlobalId, CollectionState>,
152 log_sources: BTreeMap<LogVariant, GlobalId>,
154 peeks: BTreeMap<Uuid, PendingPeek>,
163 subscribes: BTreeMap<GlobalId, ActiveSubscribe>,
177 copy_tos: BTreeSet<GlobalId>,
185 history: ComputeCommandHistory<UIntGauge>,
187 command_rx: mpsc::UnboundedReceiver<Command>,
189 response_tx: mpsc::UnboundedSender<ComputeControllerResponse>,
191 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
193 metrics: InstanceMetrics,
195 dyncfg: Arc<ConfigSet>,
197
198 peek_stash_persist_location: PersistLocation,
200
201 now: NowFn,
203 wallclock_lag: WallclockLagFn<Timestamp>,
205 wallclock_lag_last_recorded: DateTime<Utc>,
207
208 read_hold_tx: read_holds::ChangeTx,
213 replica_tx: mz_ore::channel::InstrumentedUnboundedSender<ReplicaResponse, IntCounter>,
215 replica_rx: mz_ore::channel::InstrumentedUnboundedReceiver<ReplicaResponse, IntCounter>,
217}
218
219impl Instance {
220 fn collection(&self, id: GlobalId) -> Result<&CollectionState, CollectionMissing> {
222 self.collections.get(&id).ok_or(CollectionMissing(id))
223 }
224
225 fn collection_mut(&mut self, id: GlobalId) -> Result<&mut CollectionState, CollectionMissing> {
227 self.collections.get_mut(&id).ok_or(CollectionMissing(id))
228 }
229
230 fn expect_collection(&self, id: GlobalId) -> &CollectionState {
236 self.collections.get(&id).expect("collection must exist")
237 }
238
239 fn expect_collection_mut(&mut self, id: GlobalId) -> &mut CollectionState {
245 self.collections
246 .get_mut(&id)
247 .expect("collection must exist")
248 }
249
250 fn collections_iter(&self) -> impl Iterator<Item = (GlobalId, &CollectionState)> {
251 self.collections.iter().map(|(id, coll)| (*id, coll))
252 }
253
254 fn replicas_hosting(
261 &self,
262 id: GlobalId,
263 ) -> Result<impl Iterator<Item = &ReplicaState> + Clone, CollectionMissing> {
264 let target = self.collection(id)?.target_replica;
265 Ok(self
266 .replicas
267 .values()
268 .filter(move |r| target.map_or(true, |t| t == r.id)))
269 }
270
271 fn add_collection(
277 &mut self,
278 id: GlobalId,
279 as_of: Antichain<Timestamp>,
280 shared: SharedCollectionState,
281 storage_dependencies: BTreeMap<GlobalId, ReadHold>,
282 compute_dependencies: BTreeMap<GlobalId, ReadHold>,
283 replica_input_read_holds: Vec<ReadHold>,
284 write_only: bool,
285 storage_sink: bool,
286 initial_as_of: Option<Antichain<Timestamp>>,
287 refresh_schedule: Option<RefreshSchedule>,
288 target_replica: Option<ReplicaId>,
289 ) {
290 let dependency_ids: Vec<GlobalId> = compute_dependencies
292 .keys()
293 .chain(storage_dependencies.keys())
294 .copied()
295 .collect();
296 let introspection = CollectionIntrospection::new(
297 id,
298 self.introspection_tx.clone(),
299 as_of.clone(),
300 storage_sink,
301 initial_as_of,
302 refresh_schedule,
303 dependency_ids,
304 );
305 let mut state = CollectionState::new(
306 id,
307 as_of.clone(),
308 shared,
309 storage_dependencies,
310 compute_dependencies,
311 Arc::clone(&self.read_hold_tx),
312 introspection,
313 );
314 state.target_replica = target_replica;
315 if write_only {
317 state.read_policy = None;
318 }
319
320 if let Some(previous) = self.collections.insert(id, state) {
321 panic!("attempt to add a collection with existing ID {id} (previous={previous:?}");
322 }
323
324 for replica in self.replicas.values_mut() {
326 if target_replica.is_some_and(|id| id != replica.id) {
327 continue;
328 }
329 replica.add_collection(id, as_of.clone(), replica_input_read_holds.clone());
330 }
331 }
332
333 fn remove_collection(&mut self, id: GlobalId) {
334 for replica in self.replicas.values_mut() {
336 replica.remove_collection(id);
337 }
338
339 self.collections.remove(&id);
341 }
342
343 fn add_replica_state(
344 &mut self,
345 id: ReplicaId,
346 client: ReplicaClient,
347 config: ReplicaConfig,
348 epoch: u64,
349 dyncfg_override: ConfigUpdates,
350 ) -> Result<(), read_holds::ReadHoldIssuerHungUp> {
351 let log_ids: BTreeSet<_> = config.logging.index_logs.values().copied().collect();
352
353 let metrics = self.metrics.for_replica(id);
354 let mut replica = ReplicaState::new(
355 id,
356 client,
357 config,
358 metrics,
359 self.introspection_tx.clone(),
360 epoch,
361 dyncfg_override,
362 );
363
364 let mut shutdown_input = None;
366 for (collection_id, collection) in &self.collections {
367 if (collection.log_collection && !log_ids.contains(collection_id))
370 || collection.target_replica.is_some_and(|rid| rid != id)
371 {
372 continue;
373 }
374
375 let as_of = if collection.log_collection {
376 Antichain::from_elem(Timestamp::MIN)
381 } else {
382 collection.read_frontier().to_owned()
383 };
384
385 let mut input_read_holds = Vec::with_capacity(collection.storage_dependencies.len());
393 let mut hung_up = Vec::new();
394 for hold in collection.storage_dependencies.values() {
395 match hold.try_clone() {
396 Ok(hold) => input_read_holds.push(hold),
397 Err(read_holds::ReadHoldIssuerHungUp(input_id)) => hung_up.push(input_id),
398 }
399 }
400 if !hung_up.is_empty() {
401 tracing::error!(
402 replica_id = %id,
403 %collection_id,
404 ?hung_up,
405 "giving up on adding replica collections: storage read hold issuers hung \
406 up, the process is shutting down",
407 );
408 shutdown_input = hung_up.into_iter().next();
409 break;
410 }
411
412 replica.add_collection(*collection_id, as_of, input_read_holds);
413 }
414
415 self.replicas.insert(id, replica);
416
417 match shutdown_input {
418 Some(input_id) => Err(read_holds::ReadHoldIssuerHungUp(input_id)),
419 None => Ok(()),
420 }
421 }
422
423 fn deliver_response(&self, response: ComputeControllerResponse) {
425 let _ = self.response_tx.send(response);
428 }
429
430 fn deliver_introspection_updates(&self, type_: IntrospectionType, updates: Vec<(Row, Diff)>) {
432 let _ = self.introspection_tx.send((type_, updates));
435 }
436
437 fn replica_exists(&self, id: ReplicaId) -> bool {
439 self.replicas.contains_key(&id)
440 }
441
442 fn peeks_targeting(&self, replica_id: ReplicaId) -> impl Iterator<Item = (Uuid, &PendingPeek)> {
444 self.peeks.iter().filter_map(move |(uuid, peek)| {
445 if peek.target_replica == Some(replica_id) {
446 Some((*uuid, peek))
447 } else {
448 None
449 }
450 })
451 }
452
453 fn subscribes_targeting(&self, replica_id: ReplicaId) -> impl Iterator<Item = GlobalId> + '_ {
455 self.subscribes.keys().copied().filter(move |id| {
456 let collection = self.expect_collection(*id);
457 collection.target_replica == Some(replica_id)
458 })
459 }
460
461 fn update_frontier_introspection(&mut self) {
470 for collection in self.collections.values_mut() {
471 collection
472 .introspection
473 .observe_frontiers(&collection.read_frontier(), &collection.write_frontier());
474 }
475
476 for replica in self.replicas.values_mut() {
477 for collection in replica.collections.values_mut() {
478 collection
479 .introspection
480 .observe_frontier(&collection.write_frontier);
481 }
482 }
483 }
484
485 fn refresh_state_metrics(&self) {
494 let unscheduled_collections_count =
495 self.collections.values().filter(|c| !c.scheduled).count();
496 let connected_replica_count = self
497 .replicas
498 .values()
499 .filter(|r| r.client.is_connected())
500 .count();
501
502 self.metrics
503 .replica_count
504 .set(u64::cast_from(self.replicas.len()));
505 self.metrics
506 .collection_count
507 .set(u64::cast_from(self.collections.len()));
508 self.metrics
509 .collection_unscheduled_count
510 .set(u64::cast_from(unscheduled_collections_count));
511 self.metrics
512 .peek_count
513 .set(u64::cast_from(self.peeks.len()));
514 self.metrics
515 .subscribe_count
516 .set(u64::cast_from(self.subscribes.len()));
517 self.metrics
518 .copy_to_count
519 .set(u64::cast_from(self.copy_tos.len()));
520 self.metrics
521 .connected_replica_count
522 .set(u64::cast_from(connected_replica_count));
523 }
524
525 fn refresh_wallclock_lag(&mut self) {
544 let frontier_lag = |frontier: &Antichain<Timestamp>| match frontier.as_option() {
545 Some(ts) => (self.wallclock_lag)(ts.clone()),
546 None => Duration::ZERO,
547 };
548
549 let now_ms = (self.now)();
550 let histogram_period = WallclockLagHistogramPeriod::from_epoch_millis(now_ms, &self.dyncfg);
551 let histogram_labels = match &self.workload_class {
552 Some(wc) => [("workload_class", wc.clone())].into(),
553 None => BTreeMap::new(),
554 };
555
556 let readable_storage_collections: BTreeSet<_> = self
559 .collections
560 .keys()
561 .filter_map(|id| {
562 let frontiers = self.storage_collections.collection_frontiers(*id).ok()?;
563 PartialOrder::less_than(&frontiers.read_capabilities, &frontiers.write_frontier)
564 .then_some(*id)
565 })
566 .collect();
567
568 for (id, collection) in &mut self.collections {
570 let write_frontier = collection.write_frontier();
571 let readable = if self.storage_collections.check_exists(*id).is_ok() {
572 readable_storage_collections.contains(id)
573 } else {
574 PartialOrder::less_than(&collection.read_frontier(), &write_frontier)
575 };
576
577 if let Some(stash) = &mut collection.wallclock_lag_histogram_stash {
578 let bucket = if readable {
579 let lag = frontier_lag(&write_frontier);
580 let lag = lag.as_secs().next_power_of_two();
581 WallclockLag::Seconds(lag)
582 } else {
583 WallclockLag::Undefined
584 };
585
586 let key = (histogram_period, bucket, histogram_labels.clone());
587 *stash.entry(key).or_default() += Diff::ONE;
588 }
589 }
590
591 for replica in self.replicas.values_mut() {
593 for (id, collection) in &mut replica.collections {
594 let readable = readable_storage_collections.contains(id) || collection.hydrated();
599
600 let lag = if readable {
601 let lag = frontier_lag(&collection.write_frontier);
602 WallclockLag::Seconds(lag.as_secs())
603 } else {
604 WallclockLag::Undefined
605 };
606
607 if let Some(wallclock_lag_max) = &mut collection.wallclock_lag_max {
608 *wallclock_lag_max = (*wallclock_lag_max).max(lag);
609 }
610
611 if let Some(metrics) = &mut collection.metrics {
612 let secs = lag.unwrap_seconds_or(u64::MAX);
615 metrics.wallclock_lag.observe(secs);
616 };
617 }
618 }
619
620 self.maybe_record_wallclock_lag();
622 }
623
624 fn maybe_record_wallclock_lag(&mut self) {
632 if self.read_only {
633 return;
634 }
635
636 let duration_trunc = |datetime: DateTime<_>, interval| {
637 let td = TimeDelta::from_std(interval).ok()?;
638 datetime.duration_trunc(td).ok()
639 };
640
641 let interval = WALLCLOCK_LAG_RECORDING_INTERVAL.get(&self.dyncfg);
642 let now_dt = mz_ore::now::to_datetime((self.now)());
643 let now_trunc = duration_trunc(now_dt, interval).unwrap_or_else(|| {
644 soft_panic_or_log!("excessive wallclock lag recording interval: {interval:?}");
645 let default = WALLCLOCK_LAG_RECORDING_INTERVAL.default();
646 duration_trunc(now_dt, *default).unwrap()
647 });
648 if now_trunc <= self.wallclock_lag_last_recorded {
649 return;
650 }
651
652 let now_ts: CheckedTimestamp<_> = now_trunc.try_into().expect("must fit");
653
654 let mut history_updates = Vec::new();
655 for (replica_id, replica) in &mut self.replicas {
656 for (collection_id, collection) in &mut replica.collections {
657 let Some(wallclock_lag_max) = &mut collection.wallclock_lag_max else {
658 continue;
659 };
660
661 let max_lag = std::mem::replace(wallclock_lag_max, WallclockLag::MIN);
662 let row = Row::pack_slice(&[
663 Datum::String(&collection_id.to_string()),
664 Datum::String(&replica_id.to_string()),
665 max_lag.into_interval_datum(),
666 Datum::TimestampTz(now_ts),
667 ]);
668 history_updates.push((row, Diff::ONE));
669 }
670 }
671 if !history_updates.is_empty() {
672 self.deliver_introspection_updates(
673 IntrospectionType::WallclockLagHistory,
674 history_updates,
675 );
676 }
677
678 let mut histogram_updates = Vec::new();
679 let mut row_buf = Row::default();
680 for (collection_id, collection) in &mut self.collections {
681 let Some(stash) = &mut collection.wallclock_lag_histogram_stash else {
682 continue;
683 };
684
685 for ((period, lag, labels), count) in std::mem::take(stash) {
686 let mut packer = row_buf.packer();
687 packer.extend([
688 Datum::TimestampTz(period.start),
689 Datum::TimestampTz(period.end),
690 Datum::String(&collection_id.to_string()),
691 lag.into_uint64_datum(),
692 ]);
693 let labels = labels.iter().map(|(k, v)| (*k, Datum::String(v)));
694 packer.push_dict(labels);
695
696 histogram_updates.push((row_buf.clone(), count));
697 }
698 }
699 if !histogram_updates.is_empty() {
700 self.deliver_introspection_updates(
701 IntrospectionType::WallclockLagHistogram,
702 histogram_updates,
703 );
704 }
705
706 self.wallclock_lag_last_recorded = now_trunc;
707 }
708
709 #[mz_ore::instrument(level = "debug")]
715 pub fn collection_hydrated(&self, collection_id: GlobalId) -> Result<bool, CollectionMissing> {
716 let mut hosting_replicas = self.replicas_hosting(collection_id)?.peekable();
717 if hosting_replicas.peek().is_none() {
718 return Ok(true);
719 }
720 for replica_state in hosting_replicas {
721 if replica_state.expect_collection(collection_id).hydrated() {
722 return Ok(true);
723 }
724 }
725
726 Ok(false)
727 }
728
729 #[mz_ore::instrument(level = "debug")]
745 pub fn collections_ready_on_replicas(
746 &self,
747 target_replica_ids: Option<Vec<ReplicaId>>,
748 exclude_collections: &BTreeSet<GlobalId>,
749 allowed_lag: Option<Timestamp>,
750 reference_replica_ids: &BTreeSet<ReplicaId>,
751 ) -> Result<bool, HydrationCheckBadTarget> {
752 if self.replicas.is_empty() {
753 return Ok(true);
754 }
755 let target_replicas: BTreeSet<ReplicaId> = self
756 .replicas
757 .keys()
758 .filter_map(|id| match target_replica_ids {
759 None => Some(id.clone()),
760 Some(ref ids) if ids.contains(id) => Some(id.clone()),
761 Some(_) => None,
762 })
763 .collect();
764 if let Some(targets) = target_replica_ids {
765 if target_replicas.is_empty() {
766 return Err(HydrationCheckBadTarget(targets));
767 }
768 }
769
770 let mut unhydrated = BTreeSet::new();
771 let mut lagging_ticks = BTreeMap::new();
772 let mut awaiting_completion = BTreeSet::new();
773 for (id, _collection) in self.collections_iter() {
774 if id.is_transient() || exclude_collections.contains(&id) {
775 continue;
776 }
777
778 let replicas = self
781 .replicas_hosting(id)
782 .expect("collection must exist")
783 .map(|replica| (replica.id, replica.expect_collection(id)));
784
785 match classify_collection_readiness(
786 replicas,
787 &target_replicas,
788 reference_replica_ids,
789 allowed_lag,
790 ) {
791 CollectionReadiness::Ready => {}
792 CollectionReadiness::Lagging { lag: Some(lag) } => {
796 lagging_ticks.insert(id, lag);
797 }
798 CollectionReadiness::Lagging { lag: None } => {
799 awaiting_completion.insert(id);
800 }
801 CollectionReadiness::Unhydrated => {
802 unhydrated.insert(id);
803 }
804 }
805 }
806
807 let ready =
808 unhydrated.is_empty() && lagging_ticks.is_empty() && awaiting_completion.is_empty();
809 if !ready {
810 tracing::info!(
814 replicas = ?target_replicas,
815 reference = ?reference_replica_ids,
816 unhydrated = ?unhydrated,
817 ?lagging_ticks,
818 ?awaiting_completion,
819 ?allowed_lag,
820 "collections are not ready on any target replica",
821 );
822 }
823
824 Ok(ready)
825 }
826
827 fn cleanup_collections(&mut self) {
843 let to_remove: Vec<_> = self
844 .collections_iter()
845 .filter(|(id, collection)| {
846 collection.dropped
847 && collection.shared.lock_read_capabilities(|c| c.is_empty())
848 && self
849 .replicas
850 .values()
851 .all(|r| r.collection_frontiers_empty(*id))
852 })
853 .map(|(id, _collection)| id)
854 .collect();
855
856 for id in to_remove {
857 self.remove_collection(id);
858 }
859 }
860
861 #[mz_ore::instrument(level = "debug")]
865 pub fn dump(&self) -> Result<serde_json::Value, anyhow::Error> {
866 let Self {
873 build_info: _,
874 storage_collections: _,
875 peek_stash_persist_location: _,
876 initialized,
877 read_only,
878 workload_class,
879 replicas,
880 collections,
881 log_sources: _,
882 peeks,
883 subscribes,
884 copy_tos,
885 history: _,
886 command_rx: _,
887 response_tx: _,
888 introspection_tx: _,
889 metrics: _,
890 dyncfg: _,
891 now: _,
892 wallclock_lag: _,
893 wallclock_lag_last_recorded,
894 read_hold_tx: _,
895 replica_tx: _,
896 replica_rx: _,
897 } = self;
898
899 let replicas: BTreeMap<_, _> = replicas
900 .iter()
901 .map(|(id, replica)| Ok((id.to_string(), replica.dump()?)))
902 .collect::<Result<_, anyhow::Error>>()?;
903 let collections: BTreeMap<_, _> = collections
904 .iter()
905 .map(|(id, collection)| (id.to_string(), format!("{collection:?}")))
906 .collect();
907 let peeks: BTreeMap<_, _> = peeks
908 .iter()
909 .map(|(uuid, peek)| (uuid.to_string(), format!("{peek:?}")))
910 .collect();
911 let subscribes: BTreeMap<_, _> = subscribes
912 .iter()
913 .map(|(id, subscribe)| (id.to_string(), format!("{subscribe:?}")))
914 .collect();
915 let copy_tos: Vec<_> = copy_tos.iter().map(|id| id.to_string()).collect();
916 let wallclock_lag_last_recorded = format!("{wallclock_lag_last_recorded:?}");
917
918 Ok(serde_json::json!({
919 "initialized": initialized,
920 "read_only": read_only,
921 "workload_class": workload_class,
922 "replicas": replicas,
923 "collections": collections,
924 "peeks": peeks,
925 "subscribes": subscribes,
926 "copy_tos": copy_tos,
927 "wallclock_lag_last_recorded": wallclock_lag_last_recorded,
928 }))
929 }
930
931 pub(super) fn collection_write_frontier(
933 &self,
934 id: GlobalId,
935 ) -> Result<Antichain<Timestamp>, CollectionMissing> {
936 Ok(self.collection(id)?.write_frontier())
937 }
938}
939
940impl Instance {
941 pub(super) fn new(
942 build_info: &'static BuildInfo,
943 storage: StorageCollections,
944 peek_stash_persist_location: PersistLocation,
945 arranged_logs: Vec<(LogVariant, GlobalId, SharedCollectionState)>,
946 metrics: InstanceMetrics,
947 now: NowFn,
948 wallclock_lag: WallclockLagFn<Timestamp>,
949 dyncfg: Arc<ConfigSet>,
950 command_rx: mpsc::UnboundedReceiver<Command>,
951 response_tx: mpsc::UnboundedSender<ComputeControllerResponse>,
952 read_hold_tx: read_holds::ChangeTx,
953 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
954 read_only: bool,
955 ) -> Self {
956 let mut collections = BTreeMap::new();
957 let mut log_sources = BTreeMap::new();
958 for (log, id, shared) in arranged_logs {
959 let collection = CollectionState::new_log_collection(
960 id,
961 shared,
962 Arc::clone(&read_hold_tx),
963 introspection_tx.clone(),
964 );
965 collections.insert(id, collection);
966 log_sources.insert(log, id);
967 }
968
969 let history = ComputeCommandHistory::new(metrics.for_history());
970
971 let send_count = metrics.response_send_count.clone();
972 let recv_count = metrics.response_recv_count.clone();
973 let (replica_tx, replica_rx) = instrumented_unbounded_channel(send_count, recv_count);
974
975 let now_dt = mz_ore::now::to_datetime(now());
976
977 Self {
978 build_info,
979 storage_collections: storage,
980 peek_stash_persist_location,
981 initialized: false,
982 read_only,
983 workload_class: None,
984 replicas: Default::default(),
985 collections,
986 log_sources,
987 peeks: Default::default(),
988 subscribes: Default::default(),
989 copy_tos: Default::default(),
990 history,
991 command_rx,
992 response_tx,
993 introspection_tx,
994 metrics,
995 dyncfg,
996 now,
997 wallclock_lag,
998 wallclock_lag_last_recorded: now_dt,
999 read_hold_tx,
1000 replica_tx,
1001 replica_rx,
1002 }
1003 }
1004
1005 pub(super) async fn run(mut self) {
1006 self.send(ComputeCommand::Hello {
1007 nonce: Uuid::default(),
1010 });
1011
1012 let instance_config = InstanceConfig {
1013 peek_stash_persist_location: self.peek_stash_persist_location.clone(),
1014 logging: Default::default(),
1018 expiration_offset: Default::default(),
1019 arrangement_dictionary_compression: Default::default(),
1020 initial_config: Default::default(),
1021 };
1022
1023 self.send(ComputeCommand::CreateInstance(Box::new(instance_config)));
1024
1025 loop {
1026 tokio::select! {
1027 command = self.command_rx.recv() => match command {
1028 Some(cmd) => cmd(&mut self),
1029 None => break,
1030 },
1031 response = self.replica_rx.recv() => match response {
1032 Some(response) => self.handle_response(response),
1033 None => unreachable!("self owns a sender side of the channel"),
1034 }
1035 }
1036 }
1037 }
1038
1039 #[mz_ore::instrument(level = "debug")]
1041 pub fn update_configuration(&mut self, config_params: ComputeParameters) {
1042 if let Some(workload_class) = &config_params.workload_class {
1043 self.workload_class = workload_class.clone();
1044 }
1045
1046 let command = ComputeCommand::UpdateConfiguration(Box::new(config_params));
1047 self.send(command);
1048 }
1049
1050 #[mz_ore::instrument(level = "debug")]
1055 pub fn initialization_complete(&mut self) {
1056 if !self.initialized {
1058 self.send(ComputeCommand::InitializationComplete);
1059 self.initialized = true;
1060 }
1061 }
1062
1063 #[mz_ore::instrument(level = "debug")]
1067 pub fn allow_writes(&mut self, collection_id: GlobalId) -> Result<(), CollectionMissing> {
1068 let collection = self.collection_mut(collection_id)?;
1069
1070 if !collection.read_only {
1072 return Ok(());
1073 }
1074
1075 let as_of = collection.read_frontier();
1077
1078 if as_of.is_empty() {
1081 return Ok(());
1082 }
1083
1084 collection.read_only = false;
1085 self.send(ComputeCommand::AllowWrites(collection_id));
1086
1087 Ok(())
1088 }
1089
1090 #[mz_ore::instrument(level = "debug")]
1100 pub fn shutdown(&mut self) {
1101 let (_tx, rx) = mpsc::unbounded_channel();
1103 self.command_rx = rx;
1104
1105 let stray_replicas: Vec<_> = self.replicas.keys().collect();
1106 soft_assert_or_log!(
1107 stray_replicas.is_empty(),
1108 "dropped instance still has provisioned replicas: {stray_replicas:?}",
1109 );
1110 }
1111
1112 fn initiate_shutdown(&mut self) {
1118 let (_tx, rx) = mpsc::unbounded_channel();
1121 self.command_rx = rx;
1122 }
1123
1124 #[mz_ore::instrument(level = "debug")]
1126 fn send(&mut self, cmd: ComputeCommand) {
1127 self.history.push(cmd.clone());
1132
1133 let target_replica = self.target_replica(&cmd);
1134
1135 let dyncfg = &self.dyncfg;
1136 if let Some(rid) = target_replica {
1137 if let Some(replica) = self.replicas.get_mut(&rid) {
1138 let cmd =
1139 Self::specialize_command_for_replica(cmd, &replica.dyncfg_override, dyncfg);
1140 let _ = replica.client.send(cmd);
1141 }
1142 } else {
1143 for replica in self.replicas.values_mut() {
1144 let cmd = Self::specialize_command_for_replica(
1145 cmd.clone(),
1146 &replica.dyncfg_override,
1147 dyncfg,
1148 );
1149 let _ = replica.client.send(cmd);
1150 }
1151 }
1152 }
1153
1154 fn specialize_command_for_replica(
1162 mut cmd: ComputeCommand,
1163 dyncfg_override: &ConfigUpdates,
1164 dyncfg: &ConfigSet,
1165 ) -> ComputeCommand {
1166 match &mut cmd {
1167 ComputeCommand::UpdateConfiguration(params) => {
1168 if !dyncfg_override.updates.is_empty() {
1169 params.dyncfg_updates.extend(dyncfg_override.clone());
1170 }
1171 }
1172 ComputeCommand::CreateInstance(config) => {
1173 let mut initial = ConfigUpdates::from(dyncfg);
1174 initial.extend(dyncfg_override.clone());
1175 config.initial_config = initial;
1176 }
1177 _ => {}
1178 }
1179 cmd
1180 }
1181
1182 pub(super) fn update_replica_dyncfg_overrides(
1186 &mut self,
1187 mut overrides: BTreeMap<ReplicaId, ConfigUpdates>,
1188 ) {
1189 for (id, replica) in self.replicas.iter_mut() {
1190 replica.dyncfg_override = overrides.remove(id).unwrap_or_default();
1191 }
1192 }
1193
1194 fn target_replica(&self, cmd: &ComputeCommand) -> Option<ReplicaId> {
1202 match &cmd {
1203 ComputeCommand::Schedule(id)
1204 | ComputeCommand::AllowWrites(id)
1205 | ComputeCommand::AllowCompaction { id, .. } => {
1206 self.expect_collection(*id).target_replica
1207 }
1208 ComputeCommand::CreateDataflow(desc) => {
1209 let mut target_replica = None;
1210 for id in desc.export_ids() {
1211 if let Some(replica) = self.expect_collection(id).target_replica {
1212 if target_replica.is_some() {
1213 assert_eq!(target_replica, Some(replica));
1214 }
1215 target_replica = Some(replica);
1216 }
1217 }
1218 target_replica
1219 }
1220 ComputeCommand::Peek(_)
1222 | ComputeCommand::Hello { .. }
1223 | ComputeCommand::CreateInstance(_)
1224 | ComputeCommand::InitializationComplete
1225 | ComputeCommand::UpdateConfiguration(_)
1226 | ComputeCommand::CancelPeek { .. } => None,
1227 }
1228 }
1229
1230 #[mz_ore::instrument(level = "debug")]
1235 pub fn add_replica(
1236 &mut self,
1237 id: ReplicaId,
1238 mut config: ReplicaConfig,
1239 epoch: Option<u64>,
1240 dyncfg_override: ConfigUpdates,
1241 ) -> Result<(), ReplicaExists> {
1242 if self.replica_exists(id) {
1243 return Err(ReplicaExists(id));
1244 }
1245
1246 config.logging.index_logs = self.log_sources.clone();
1247
1248 let epoch = epoch.unwrap_or(1);
1249 let metrics = self.metrics.for_replica(id);
1250 let client = ReplicaClient::spawn(
1251 id,
1252 self.build_info,
1253 config.clone(),
1254 epoch,
1255 metrics.clone(),
1256 Arc::clone(&self.dyncfg),
1257 self.replica_tx.clone(),
1258 );
1259
1260 self.history.reduce();
1262
1263 self.history.update_source_uppers(&self.storage_collections);
1265
1266 for command in self.history.iter() {
1268 if let Some(target_replica) = self.target_replica(command)
1270 && target_replica != id
1271 {
1272 continue;
1273 }
1274
1275 let command = Self::specialize_command_for_replica(
1278 command.clone(),
1279 &dyncfg_override,
1280 &self.dyncfg,
1281 );
1282 if client.send(command).is_err() {
1283 tracing::warn!("Replica {:?} connection terminated during hydration", id);
1286 break;
1287 }
1288 }
1289
1290 if self
1292 .add_replica_state(id, client, config, epoch, dyncfg_override)
1293 .is_err()
1294 {
1295 self.initiate_shutdown();
1301 }
1302
1303 Ok(())
1304 }
1305
1306 #[mz_ore::instrument(level = "debug")]
1308 pub fn remove_replica(&mut self, id: ReplicaId) -> Result<(), ReplicaMissing> {
1309 let replica = self.replicas.remove(&id).ok_or(ReplicaMissing(id))?;
1310
1311 for (collection_id, replica_collection) in &replica.collections {
1319 let collection = self.collections.get(collection_id);
1320 for replica_hold in &replica_collection.input_read_holds {
1321 let input_id = replica_hold.id();
1322 let global_hold = collection.and_then(|c| c.storage_dependencies.get(&input_id));
1323 let unprotected = global_hold
1324 .is_none_or(|h| PartialOrder::less_than(replica_hold.since(), h.since()));
1325 if unprotected {
1326 tracing::warn!(
1327 replica_id = %id,
1328 %collection_id,
1329 %input_id,
1330 replica_hold_since = ?replica_hold.since(),
1331 global_hold_since = ?global_hold.map(|h| h.since()),
1332 "dropping per-replica read hold without equivalent global read hold",
1333 );
1334 }
1335 }
1336 }
1337 drop(replica);
1338
1339 let to_drop: Vec<_> = self.subscribes_targeting(id).collect();
1343 for subscribe_id in to_drop {
1344 let subscribe = self.subscribes.remove(&subscribe_id).unwrap();
1345 let response = ComputeControllerResponse::SubscribeResponse(
1346 subscribe_id,
1347 SubscribeBatch {
1348 lower: subscribe.frontier.clone(),
1349 upper: subscribe.frontier,
1350 updates: Err(ERROR_TARGET_REPLICA_FAILED.into()),
1351 },
1352 );
1353 self.deliver_response(response);
1354 }
1355
1356 let mut peek_responses = Vec::new();
1361 let mut to_drop = Vec::new();
1362 for (uuid, peek) in self.peeks_targeting(id) {
1363 peek_responses.push(ComputeControllerResponse::PeekNotification(
1364 uuid,
1365 PeekNotification::Error(ERROR_TARGET_REPLICA_FAILED.into()),
1366 peek.otel_ctx.clone(),
1367 ));
1368 to_drop.push(uuid);
1369 }
1370 for response in peek_responses {
1371 self.deliver_response(response);
1372 }
1373 for uuid in to_drop {
1374 let response =
1375 PeekResponse::Error(ProtocolPeekError::unstructured(ERROR_TARGET_REPLICA_FAILED));
1376 self.finish_peek(uuid, response);
1377 }
1378
1379 self.forward_implied_capabilities();
1382
1383 Ok(())
1384 }
1385
1386 fn rehydrate_replica(&mut self, id: ReplicaId) {
1392 let replica = &self.replicas[&id];
1393 let config = replica.config.clone();
1394 let epoch = replica.epoch + 1;
1395 let dyncfg_override = replica.dyncfg_override.clone();
1396
1397 self.remove_replica(id).expect("replica must exist");
1398 let result = self.add_replica(id, config, Some(epoch), dyncfg_override);
1399
1400 match result {
1401 Ok(()) => (),
1402 Err(ReplicaExists(_)) => unreachable!("replica was removed"),
1403 }
1404 }
1405
1406 fn rehydrate_failed_replicas(&mut self) {
1408 let replicas = self.replicas.iter();
1409 let failed_replicas: Vec<_> = replicas
1410 .filter_map(|(id, replica)| replica.client.is_failed().then_some(*id))
1411 .collect();
1412
1413 for replica_id in failed_replicas {
1414 self.rehydrate_replica(replica_id);
1415 }
1416 }
1417
1418 #[mz_ore::instrument(level = "debug")]
1423 pub fn create_dataflow(
1424 &mut self,
1425 dataflow: LirDataflowDescription,
1426 import_read_holds: Vec<ReadHold>,
1427 mut shared_collection_state: BTreeMap<GlobalId, SharedCollectionState>,
1428 target_replica: Option<ReplicaId>,
1429 ) -> Result<(), DataflowCreationError> {
1430 use DataflowCreationError::*;
1431
1432 if let Some(replica_id) = target_replica {
1436 if !self.replica_exists(replica_id) {
1437 return Err(ReplicaMissing(replica_id));
1438 }
1439 }
1440
1441 let as_of = dataflow.as_of.as_ref().ok_or(MissingAsOf)?;
1443 if as_of.is_empty() && dataflow.subscribe_ids().next().is_some() {
1444 return Err(EmptyAsOfForSubscribe);
1445 }
1446 if as_of.is_empty() && dataflow.copy_to_ids().next().is_some() {
1447 return Err(EmptyAsOfForCopyTo);
1448 }
1449
1450 let mut storage_dependencies = BTreeMap::new();
1452 let mut compute_dependencies = BTreeMap::new();
1453
1454 let mut replica_input_read_holds = Vec::new();
1459
1460 let mut import_read_holds: BTreeMap<_, _> =
1461 import_read_holds.into_iter().map(|r| (r.id(), r)).collect();
1462
1463 for &id in dataflow.source_imports.keys() {
1464 let mut read_hold = import_read_holds.remove(&id).ok_or(ReadHoldMissing(id))?;
1465 replica_input_read_holds.push(read_hold.clone());
1466
1467 read_hold
1468 .try_downgrade(as_of.clone())
1469 .map_err(|_| ReadHoldInsufficient(id))?;
1470 storage_dependencies.insert(id, read_hold);
1471 }
1472
1473 for &id in dataflow.index_imports.keys() {
1474 let mut read_hold = import_read_holds.remove(&id).ok_or(ReadHoldMissing(id))?;
1475 read_hold
1476 .try_downgrade(as_of.clone())
1477 .map_err(|_| ReadHoldInsufficient(id))?;
1478 compute_dependencies.insert(id, read_hold);
1479 }
1480
1481 if as_of.is_empty() {
1484 replica_input_read_holds = Default::default();
1485 }
1486
1487 for export_id in dataflow.export_ids() {
1489 let shared = shared_collection_state
1490 .remove(&export_id)
1491 .unwrap_or_else(|| SharedCollectionState::new(as_of.clone()));
1492 let write_only = dataflow.sink_exports.contains_key(&export_id);
1493 let storage_sink = dataflow.persist_sink_ids().any(|id| id == export_id);
1494
1495 self.add_collection(
1496 export_id,
1497 as_of.clone(),
1498 shared,
1499 storage_dependencies.clone(),
1500 compute_dependencies.clone(),
1501 replica_input_read_holds.clone(),
1502 write_only,
1503 storage_sink,
1504 dataflow.initial_storage_as_of.clone(),
1505 dataflow.refresh_schedule.clone(),
1506 target_replica,
1507 );
1508
1509 if let Ok(frontiers) = self.storage_collections.collection_frontiers(export_id) {
1512 self.maybe_update_global_write_frontier(export_id, frontiers.write_frontier);
1513 }
1514 }
1515
1516 for subscribe_id in dataflow.subscribe_ids() {
1518 self.subscribes
1519 .insert(subscribe_id, ActiveSubscribe::default());
1520 }
1521
1522 for copy_to_id in dataflow.copy_to_ids() {
1524 self.copy_tos.insert(copy_to_id);
1525 }
1526
1527 let mut source_imports = BTreeMap::new();
1530 for (id, import) in dataflow.source_imports {
1531 let frontiers = self
1532 .storage_collections
1533 .collection_frontiers(id)
1534 .expect("collection exists");
1535
1536 let collection_metadata = self
1537 .storage_collections
1538 .collection_metadata(id)
1539 .expect("we have a read hold on this collection");
1540
1541 let desc = SourceInstanceDesc {
1542 storage_metadata: collection_metadata.clone(),
1543 arguments: import.desc.arguments,
1544 typ: import.desc.typ.clone(),
1545 };
1546 source_imports.insert(
1547 id,
1548 mz_compute_types::dataflows::SourceImport {
1549 desc,
1550 monotonic: import.monotonic,
1551 with_snapshot: import.with_snapshot,
1552 upper: frontiers.write_frontier,
1553 },
1554 );
1555 }
1556
1557 let mut sink_exports = BTreeMap::new();
1558 for (id, se) in dataflow.sink_exports {
1559 let connection = match se.connection {
1560 ComputeSinkConnection::MaterializedView(conn) => {
1561 let metadata = self
1562 .storage_collections
1563 .collection_metadata(id)
1564 .map_err(|_| CollectionMissing(id))?
1565 .clone();
1566 let conn = MaterializedViewSinkConnection {
1567 value_desc: conn.value_desc,
1568 storage_metadata: metadata,
1569 };
1570 ComputeSinkConnection::MaterializedView(conn)
1571 }
1572 ComputeSinkConnection::Subscribe(conn) => ComputeSinkConnection::Subscribe(conn),
1573 ComputeSinkConnection::CopyToS3Oneshot(conn) => {
1574 ComputeSinkConnection::CopyToS3Oneshot(conn)
1575 }
1576 ComputeSinkConnection::MetricSink(conn) => ComputeSinkConnection::MetricSink(conn),
1577 };
1578 let desc = ComputeSinkDesc {
1579 from: se.from,
1580 from_desc: se.from_desc,
1581 connection,
1582 with_snapshot: se.with_snapshot,
1583 up_to: se.up_to,
1584 non_null_assertions: se.non_null_assertions,
1585 refresh_schedule: se.refresh_schedule,
1586 from_arrangement: se.from_arrangement,
1587 };
1588 sink_exports.insert(id, desc);
1589 }
1590
1591 let objects_to_build = dataflow
1593 .objects_to_build
1594 .into_iter()
1595 .map(|object| BuildDesc {
1596 id: object.id,
1597 plan: RenderPlan::try_from(object.plan).expect("valid plan"),
1598 })
1599 .collect();
1600
1601 let augmented_dataflow = DataflowDescription {
1602 source_imports,
1603 sink_exports,
1604 objects_to_build,
1605 index_imports: dataflow.index_imports,
1607 index_exports: dataflow.index_exports,
1608 as_of: dataflow.as_of.clone(),
1609 until: dataflow.until,
1610 initial_storage_as_of: dataflow.initial_storage_as_of,
1611 refresh_schedule: dataflow.refresh_schedule,
1612 debug_name: dataflow.debug_name,
1613 time_dependence: dataflow.time_dependence,
1614 };
1615
1616 if augmented_dataflow.is_transient() {
1617 tracing::debug!(
1618 name = %augmented_dataflow.debug_name,
1619 import_ids = %augmented_dataflow.display_import_ids(),
1620 export_ids = %augmented_dataflow.display_export_ids(),
1621 as_of = ?augmented_dataflow.as_of.as_ref().unwrap().elements(),
1622 until = ?augmented_dataflow.until.elements(),
1623 "creating dataflow",
1624 );
1625 } else {
1626 tracing::info!(
1627 name = %augmented_dataflow.debug_name,
1628 import_ids = %augmented_dataflow.display_import_ids(),
1629 export_ids = %augmented_dataflow.display_export_ids(),
1630 as_of = ?augmented_dataflow.as_of.as_ref().unwrap().elements(),
1631 until = ?augmented_dataflow.until.elements(),
1632 "creating dataflow",
1633 );
1634 }
1635
1636 if as_of.is_empty() {
1639 tracing::info!(
1640 name = %augmented_dataflow.debug_name,
1641 "not sending `CreateDataflow`, because of empty `as_of`",
1642 );
1643 } else {
1644 let collections: Vec<_> = augmented_dataflow.export_ids().collect();
1645 self.send(ComputeCommand::CreateDataflow(Box::new(augmented_dataflow)));
1646
1647 for id in collections {
1648 self.maybe_schedule_collection(id);
1649 }
1650 }
1651
1652 Ok(())
1653 }
1654
1655 fn maybe_schedule_collection(&mut self, id: GlobalId) {
1661 let collection = self.expect_collection(id);
1662
1663 if collection.scheduled {
1665 return;
1666 }
1667
1668 let as_of = collection.read_frontier();
1669
1670 if as_of.is_empty() {
1673 return;
1674 }
1675
1676 let ready = if id.is_transient() {
1677 true
1683 } else {
1684 let not_self_dep = |x: &GlobalId| *x != id;
1690
1691 let mut deps_scheduled = true;
1694
1695 let compute_deps = collection.compute_dependency_ids().filter(not_self_dep);
1700 let mut compute_frontiers = Vec::new();
1701 for id in compute_deps {
1702 let dep = &self.expect_collection(id);
1703 deps_scheduled &= dep.scheduled;
1704 compute_frontiers.push(dep.write_frontier());
1705 }
1706
1707 let storage_deps = collection.storage_dependency_ids().filter(not_self_dep);
1708 let storage_frontiers = self
1709 .storage_collections
1710 .collections_frontiers(storage_deps.collect())
1711 .expect("must exist");
1712 let storage_frontiers = storage_frontiers.into_iter().map(|f| f.write_frontier);
1713
1714 let mut frontiers = compute_frontiers.into_iter().chain(storage_frontiers);
1715 let frontiers_ready =
1716 frontiers.all(|frontier| PartialOrder::less_than(&as_of, &frontier));
1717
1718 deps_scheduled && frontiers_ready
1719 };
1720
1721 if ready {
1722 self.send(ComputeCommand::Schedule(id));
1723 let collection = self.expect_collection_mut(id);
1724 collection.scheduled = true;
1725 }
1726 }
1727
1728 fn schedule_collections(&mut self) {
1730 let ids: Vec<_> = self.collections.keys().copied().collect();
1731 for id in ids {
1732 self.maybe_schedule_collection(id);
1733 }
1734 }
1735
1736 #[mz_ore::instrument(level = "debug")]
1739 pub fn drop_collections(&mut self, ids: Vec<GlobalId>) -> Result<(), CollectionMissing> {
1740 for id in &ids {
1741 let collection = self.collection_mut(*id)?;
1742
1743 collection.dropped = true;
1745
1746 collection.implied_read_hold.release();
1749 collection.warmup_read_hold.release();
1750
1751 self.subscribes.remove(id);
1754 self.copy_tos.remove(id);
1757 }
1758
1759 Ok(())
1760 }
1761
1762 #[mz_ore::instrument(level = "debug")]
1766 pub fn peek(
1767 &mut self,
1768 peek_target: PeekTarget,
1769 literal_constraints: Option<Vec<Row>>,
1770 uuid: Uuid,
1771 timestamp: Timestamp,
1772 result_desc: RelationDesc,
1773 finishing: RowSetFinishing,
1774 map_filter_project: mz_expr::SafeMfpPlan,
1775 mut read_hold: ReadHold,
1776 target_replica: Option<ReplicaId>,
1777 peek_response_tx: oneshot::Sender<PeekResponse>,
1778 ) -> Result<(), PeekError> {
1779 use PeekError::*;
1780
1781 let target_id = peek_target.id();
1782
1783 if read_hold.id() != target_id {
1785 return Err(ReadHoldIdMismatch(read_hold.id()));
1786 }
1787 read_hold
1788 .try_downgrade(Antichain::from_elem(timestamp.clone()))
1789 .map_err(|_| ReadHoldInsufficient(target_id))?;
1790
1791 if let Some(target) = target_replica {
1792 if !self.replica_exists(target) {
1793 return Err(ReplicaMissing(target));
1794 }
1795 }
1796
1797 let otel_ctx = OpenTelemetryContext::obtain();
1798
1799 self.peeks.insert(
1800 uuid,
1801 PendingPeek {
1802 target_replica,
1803 otel_ctx: otel_ctx.clone(),
1805 requested_at: Instant::now(),
1806 read_hold,
1807 peek_response_tx,
1808 limit: finishing.limit.map(usize::cast_from),
1809 offset: finishing.offset,
1810 },
1811 );
1812
1813 let peek = Peek {
1814 literal_constraints,
1815 uuid,
1816 timestamp,
1817 finishing,
1818 map_filter_project,
1819 otel_ctx,
1822 target: peek_target,
1823 result_desc,
1824 };
1825 self.send(ComputeCommand::Peek(Box::new(peek)));
1826
1827 Ok(())
1828 }
1829
1830 #[mz_ore::instrument(level = "debug")]
1832 pub fn cancel_peek(&mut self, uuid: Uuid, reason: PeekResponse) {
1833 let Some(peek) = self.peeks.get_mut(&uuid) else {
1834 tracing::warn!("did not find pending peek for {uuid}");
1835 return;
1836 };
1837
1838 let duration = peek.requested_at.elapsed();
1839 self.metrics
1840 .observe_peek_response(&PeekResponse::Canceled, duration);
1841
1842 let otel_ctx = peek.otel_ctx.clone();
1844 otel_ctx.attach_as_parent();
1845
1846 self.deliver_response(ComputeControllerResponse::PeekNotification(
1847 uuid,
1848 PeekNotification::Canceled,
1849 otel_ctx,
1850 ));
1851
1852 self.finish_peek(uuid, reason);
1855 }
1856
1857 #[mz_ore::instrument(level = "debug")]
1869 pub fn set_read_policy(
1870 &mut self,
1871 policies: Vec<(GlobalId, ReadPolicy)>,
1872 ) -> Result<(), ReadPolicyError> {
1873 for (id, _policy) in &policies {
1876 let collection = self.collection(*id)?;
1877 if collection.read_policy.is_none() {
1878 return Err(ReadPolicyError::WriteOnlyCollection(*id));
1879 }
1880 }
1881
1882 for (id, new_policy) in policies {
1883 let collection = self.expect_collection_mut(id);
1884 let new_since = new_policy.frontier(collection.write_frontier().borrow());
1885 let _ = collection.implied_read_hold.try_downgrade(new_since);
1886 collection.read_policy = Some(new_policy);
1887 }
1888
1889 Ok(())
1890 }
1891
1892 #[mz_ore::instrument(level = "debug")]
1900 fn maybe_update_global_write_frontier(
1901 &mut self,
1902 id: GlobalId,
1903 new_frontier: Antichain<Timestamp>,
1904 ) {
1905 let collection = self.expect_collection_mut(id);
1906
1907 let advanced = collection.shared.lock_write_frontier(|f| {
1908 let advanced = PartialOrder::less_than(f, &new_frontier);
1909 if advanced {
1910 f.clone_from(&new_frontier);
1911 }
1912 advanced
1913 });
1914
1915 if !advanced {
1916 return;
1917 }
1918
1919 let new_since = match &collection.read_policy {
1921 Some(read_policy) => {
1922 read_policy.frontier(new_frontier.borrow())
1925 }
1926 None => {
1927 Antichain::from_iter(
1936 new_frontier
1937 .iter()
1938 .map(|t| t.step_back().unwrap_or(Timestamp::MIN)),
1939 )
1940 }
1941 };
1942 let _ = collection.implied_read_hold.try_downgrade(new_since);
1943
1944 self.deliver_response(ComputeControllerResponse::FrontierUpper {
1946 id,
1947 upper: new_frontier,
1948 });
1949 }
1950
1951 pub(super) fn apply_read_hold_change(
1953 &mut self,
1954 id: GlobalId,
1955 mut update: ChangeBatch<Timestamp>,
1956 ) {
1957 let Some(collection) = self.collections.get_mut(&id) else {
1958 soft_panic_or_log!(
1959 "read hold change for absent collection (id={id}, changes={update:?})"
1960 );
1961 return;
1962 };
1963
1964 let new_since = collection.shared.lock_read_capabilities(|caps| {
1965 let read_frontier = caps.frontier();
1968 for (time, diff) in update.iter() {
1969 let count = caps.count_for(time) + diff;
1970 assert!(
1971 count >= 0,
1972 "invalid read capabilities update: negative capability \
1973 (id={id:?}, read_capabilities={caps:?}, update={update:?})",
1974 );
1975 assert!(
1976 count == 0 || read_frontier.less_equal(time),
1977 "invalid read capabilities update: frontier regression \
1978 (id={id:?}, read_capabilities={caps:?}, update={update:?})",
1979 );
1980 }
1981
1982 let changes = caps.update_iter(update.drain());
1985
1986 let changed = changes.count() > 0;
1987 changed.then(|| caps.frontier().to_owned())
1988 });
1989
1990 let Some(new_since) = new_since else {
1991 return; };
1993
1994 for read_hold in collection.compute_dependencies.values_mut() {
1996 read_hold
1997 .try_downgrade(new_since.clone())
1998 .expect("frontiers don't regress");
1999 }
2000 for read_hold in collection.storage_dependencies.values_mut() {
2001 read_hold
2002 .try_downgrade(new_since.clone())
2003 .expect("frontiers don't regress");
2004 }
2005
2006 self.send(ComputeCommand::AllowCompaction {
2008 id,
2009 frontier: new_since,
2010 });
2011 }
2012
2013 fn finish_peek(&mut self, uuid: Uuid, response: PeekResponse) {
2022 let Some(peek) = self.peeks.remove(&uuid) else {
2023 return;
2024 };
2025
2026 let _ = peek.peek_response_tx.send(response);
2028
2029 self.send(ComputeCommand::CancelPeek { uuid });
2032
2033 drop(peek.read_hold);
2034 }
2035
2036 fn handle_response(&mut self, (replica_id, epoch, response): ReplicaResponse) {
2039 if self
2041 .replicas
2042 .get(&replica_id)
2043 .filter(|replica| replica.epoch == epoch)
2044 .is_none()
2045 {
2046 return;
2047 }
2048
2049 match response {
2052 ComputeResponse::Frontiers(id, frontiers) => {
2053 self.handle_frontiers_response(id, frontiers, replica_id);
2054 }
2055 ComputeResponse::PeekResponse(uuid, peek_response, otel_ctx) => {
2056 self.handle_peek_response(uuid, peek_response, otel_ctx, replica_id);
2057 }
2058 ComputeResponse::CopyToResponse(id, response) => {
2059 self.handle_copy_to_response(id, response, replica_id);
2060 }
2061 ComputeResponse::SubscribeResponse(id, response) => {
2062 self.handle_subscribe_response(id, response, replica_id);
2063 }
2064 ComputeResponse::Status(response) => {
2065 self.handle_status_response(response, replica_id);
2066 }
2067 }
2068 }
2069
2070 fn handle_frontiers_response(
2073 &mut self,
2074 id: GlobalId,
2075 frontiers: FrontiersResponse,
2076 replica_id: ReplicaId,
2077 ) {
2078 if !self.collections.contains_key(&id) {
2079 soft_panic_or_log!(
2080 "frontiers update for an unknown collection \
2081 (id={id}, replica_id={replica_id}, frontiers={frontiers:?})"
2082 );
2083 return;
2084 }
2085 let Some(replica) = self.replicas.get_mut(&replica_id) else {
2086 soft_panic_or_log!(
2087 "frontiers update for an unknown replica \
2088 (replica_id={replica_id}, frontiers={frontiers:?})"
2089 );
2090 return;
2091 };
2092 let Some(replica_collection) = replica.collections.get_mut(&id) else {
2093 soft_panic_or_log!(
2094 "frontiers update for an unknown replica collection \
2095 (id={id}, replica_id={replica_id}, frontiers={frontiers:?})"
2096 );
2097 return;
2098 };
2099
2100 if let Some(new_frontier) = frontiers.input_frontier {
2101 replica_collection.update_input_frontier(new_frontier.clone());
2102 }
2103 if let Some(new_frontier) = frontiers.output_frontier {
2104 replica_collection.update_output_frontier(new_frontier.clone());
2105 }
2106 if let Some(new_frontier) = frontiers.write_frontier {
2107 replica_collection.update_write_frontier(new_frontier.clone());
2108 self.maybe_update_global_write_frontier(id, new_frontier);
2109 }
2110 }
2111
2112 #[mz_ore::instrument(level = "debug")]
2113 fn handle_peek_response(
2114 &mut self,
2115 uuid: Uuid,
2116 response: PeekResponse,
2117 otel_ctx: OpenTelemetryContext,
2118 replica_id: ReplicaId,
2119 ) {
2120 otel_ctx.attach_as_parent();
2121
2122 let Some(peek) = self.peeks.get(&uuid) else {
2125 return;
2126 };
2127
2128 let target_replica = peek.target_replica.unwrap_or(replica_id);
2130 if target_replica != replica_id {
2131 return;
2132 }
2133
2134 let duration = peek.requested_at.elapsed();
2135 self.metrics.observe_peek_response(&response, duration);
2136
2137 let notification = PeekNotification::new(&response, peek.offset, peek.limit);
2138 self.deliver_response(ComputeControllerResponse::PeekNotification(
2141 uuid,
2142 notification,
2143 otel_ctx,
2144 ));
2145
2146 self.finish_peek(uuid, response)
2147 }
2148
2149 fn handle_copy_to_response(
2150 &mut self,
2151 sink_id: GlobalId,
2152 response: CopyToResponse,
2153 replica_id: ReplicaId,
2154 ) {
2155 if !self.collections.contains_key(&sink_id) {
2156 soft_panic_or_log!(
2157 "received response for an unknown copy-to \
2158 (sink_id={sink_id}, replica_id={replica_id})",
2159 );
2160 return;
2161 }
2162 let Some(replica) = self.replicas.get_mut(&replica_id) else {
2163 soft_panic_or_log!("copy-to response for an unknown replica (replica_id={replica_id})");
2164 return;
2165 };
2166 let Some(replica_collection) = replica.collections.get_mut(&sink_id) else {
2167 soft_panic_or_log!(
2168 "copy-to response for an unknown replica collection \
2169 (sink_id={sink_id}, replica_id={replica_id})"
2170 );
2171 return;
2172 };
2173
2174 replica_collection.update_write_frontier(Antichain::new());
2178 replica_collection.update_input_frontier(Antichain::new());
2179 replica_collection.update_output_frontier(Antichain::new());
2180
2181 if !self.copy_tos.remove(&sink_id) {
2184 return;
2185 }
2186
2187 let result = match response {
2188 CopyToResponse::RowCount(count) => Ok(count),
2189 CopyToResponse::Error(error) => Err(anyhow::anyhow!(error)),
2190 CopyToResponse::Dropped => {
2195 tracing::error!(
2196 %sink_id, %replica_id,
2197 "received `Dropped` response for a tracked copy to",
2198 );
2199 return;
2200 }
2201 };
2202
2203 self.deliver_response(ComputeControllerResponse::CopyToResponse(sink_id, result));
2204 }
2205
2206 fn handle_subscribe_response(
2207 &mut self,
2208 subscribe_id: GlobalId,
2209 response: SubscribeResponse,
2210 replica_id: ReplicaId,
2211 ) {
2212 if !self.collections.contains_key(&subscribe_id) {
2213 soft_panic_or_log!(
2214 "received response for an unknown subscribe \
2215 (subscribe_id={subscribe_id}, replica_id={replica_id})",
2216 );
2217 return;
2218 }
2219 let Some(replica) = self.replicas.get_mut(&replica_id) else {
2220 soft_panic_or_log!(
2221 "subscribe response for an unknown replica (replica_id={replica_id})"
2222 );
2223 return;
2224 };
2225 let Some(replica_collection) = replica.collections.get_mut(&subscribe_id) else {
2226 soft_panic_or_log!(
2227 "subscribe response for an unknown replica collection \
2228 (subscribe_id={subscribe_id}, replica_id={replica_id})"
2229 );
2230 return;
2231 };
2232
2233 let write_frontier = match &response {
2237 SubscribeResponse::Batch(batch) => batch.upper.clone(),
2238 SubscribeResponse::DroppedAt(_) => Antichain::new(),
2239 };
2240
2241 replica_collection.update_write_frontier(write_frontier.clone());
2245 replica_collection.update_input_frontier(write_frontier.clone());
2246 replica_collection.update_output_frontier(write_frontier.clone());
2247
2248 let Some(mut subscribe) = self.subscribes.get(&subscribe_id).cloned() else {
2250 return;
2251 };
2252
2253 self.maybe_update_global_write_frontier(subscribe_id, write_frontier);
2259
2260 match response {
2261 SubscribeResponse::Batch(batch) => {
2262 let upper = batch.upper;
2263 let mut updates = batch.updates;
2264
2265 if PartialOrder::less_than(&subscribe.frontier, &upper) {
2268 let lower = std::mem::replace(&mut subscribe.frontier, upper.clone());
2269
2270 if upper.is_empty() {
2271 self.subscribes.remove(&subscribe_id);
2273 } else {
2274 self.subscribes.insert(subscribe_id, subscribe);
2276 }
2277
2278 if let Ok(updates) = updates.as_mut() {
2279 updates.retain_mut(|updates| {
2280 let offset = updates.times().partition_point(|t| {
2281 !lower.less_equal(t)
2284 });
2285 let (_, past_lower) = std::mem::take(updates).split_at(offset);
2286 *updates = past_lower;
2287 updates.len() > 0
2288 });
2289 }
2290 self.deliver_response(ComputeControllerResponse::SubscribeResponse(
2291 subscribe_id,
2292 SubscribeBatch {
2293 lower,
2294 upper,
2295 updates,
2296 },
2297 ));
2298 }
2299 }
2300 SubscribeResponse::DroppedAt(frontier) => {
2301 tracing::error!(
2306 %subscribe_id,
2307 %replica_id,
2308 frontier = ?frontier.elements(),
2309 "received `DroppedAt` response for a tracked subscribe",
2310 );
2311 self.subscribes.remove(&subscribe_id);
2312 }
2313 }
2314 }
2315
2316 fn handle_status_response(&self, response: StatusResponse, _replica_id: ReplicaId) {
2317 match response {
2318 StatusResponse::Placeholder => {}
2319 }
2320 }
2321
2322 fn dependency_write_frontiers<'b>(
2324 &'b self,
2325 collection: &'b CollectionState,
2326 ) -> impl Iterator<Item = Antichain<Timestamp>> + 'b {
2327 let compute_frontiers = collection.compute_dependency_ids().filter_map(|dep_id| {
2328 let collection = self.collections.get(&dep_id);
2329 collection.map(|c| c.write_frontier())
2330 });
2331 let storage_frontiers = collection.storage_dependency_ids().filter_map(|dep_id| {
2332 let frontiers = self.storage_collections.collection_frontiers(dep_id).ok();
2333 frontiers.map(|f| f.write_frontier)
2334 });
2335
2336 compute_frontiers.chain(storage_frontiers)
2337 }
2338
2339 fn transitive_storage_dependency_write_frontiers<'b>(
2341 &'b self,
2342 collection: &'b CollectionState,
2343 ) -> impl Iterator<Item = Antichain<Timestamp>> + 'b {
2344 let mut storage_ids: BTreeSet<_> = collection.storage_dependency_ids().collect();
2345 let mut todo: Vec<_> = collection.compute_dependency_ids().collect();
2346 let mut done = BTreeSet::new();
2347
2348 while let Some(id) = todo.pop() {
2349 if done.contains(&id) {
2350 continue;
2351 }
2352 if let Some(dep) = self.collections.get(&id) {
2353 storage_ids.extend(dep.storage_dependency_ids());
2354 todo.extend(dep.compute_dependency_ids())
2355 }
2356 done.insert(id);
2357 }
2358
2359 let storage_frontiers = storage_ids.into_iter().filter_map(|id| {
2360 let frontiers = self.storage_collections.collection_frontiers(id).ok();
2361 frontiers.map(|f| f.write_frontier)
2362 });
2363
2364 storage_frontiers
2365 }
2366
2367 fn downgrade_warmup_capabilities(&mut self) {
2380 let mut new_capabilities = BTreeMap::new();
2381 for (id, collection) in &self.collections {
2382 if collection.read_policy.is_none()
2386 && collection.shared.lock_write_frontier(|f| f.is_empty())
2387 {
2388 new_capabilities.insert(*id, Antichain::new());
2389 continue;
2390 }
2391
2392 let mut new_capability = Antichain::new();
2393 for frontier in self.dependency_write_frontiers(collection) {
2394 for time in frontier {
2395 new_capability.insert(time.step_back().unwrap_or(time));
2396 }
2397 }
2398
2399 new_capabilities.insert(*id, new_capability);
2400 }
2401
2402 for (id, new_capability) in new_capabilities {
2403 let collection = self.expect_collection_mut(id);
2404 let _ = collection.warmup_read_hold.try_downgrade(new_capability);
2405 }
2406 }
2407
2408 fn forward_implied_capabilities(&mut self) {
2436 if !ENABLE_PAUSED_CLUSTER_READHOLD_DOWNGRADE.get(&self.dyncfg) {
2437 return;
2438 }
2439 if !self.replicas.is_empty() {
2440 return;
2441 }
2442
2443 let mut new_capabilities = BTreeMap::new();
2444 for (id, collection) in &self.collections {
2445 let Some(read_policy) = &collection.read_policy else {
2446 continue;
2448 };
2449
2450 let mut dep_frontier = Antichain::new();
2454 for frontier in self.transitive_storage_dependency_write_frontiers(collection) {
2455 dep_frontier.extend(frontier);
2456 }
2457
2458 let new_capability = read_policy.frontier(dep_frontier.borrow());
2459 if PartialOrder::less_than(collection.implied_read_hold.since(), &new_capability) {
2460 new_capabilities.insert(*id, new_capability);
2461 }
2462 }
2463
2464 for (id, new_capability) in new_capabilities {
2465 let collection = self.expect_collection_mut(id);
2466 let _ = collection.implied_read_hold.try_downgrade(new_capability);
2467 }
2468 }
2469
2470 pub(super) fn acquire_read_hold(&self, id: GlobalId) -> Result<ReadHold, CollectionMissing> {
2475 let collection = self.collection(id)?;
2481 let since = collection.shared.lock_read_capabilities(|caps| {
2482 let since = caps.frontier().to_owned();
2483 caps.update_iter(since.iter().map(|t| (t.clone(), 1)));
2484 since
2485 });
2486 let hold = ReadHold::new(id, since, Arc::clone(&self.read_hold_tx));
2487 Ok(hold)
2488 }
2489
2490 #[mz_ore::instrument(level = "debug")]
2496 pub fn maintain(&mut self) {
2497 self.rehydrate_failed_replicas();
2498 self.downgrade_warmup_capabilities();
2499 self.forward_implied_capabilities();
2500 self.schedule_collections();
2501 self.cleanup_collections();
2502 self.update_frontier_introspection();
2503 self.refresh_state_metrics();
2504 self.refresh_wallclock_lag();
2505 }
2506}
2507
2508#[derive(Debug)]
2513struct CollectionState {
2514 target_replica: Option<ReplicaId>,
2516 log_collection: bool,
2520 dropped: bool,
2526 scheduled: bool,
2529
2530 read_only: bool,
2534
2535 shared: SharedCollectionState,
2537
2538 implied_read_hold: ReadHold,
2545 warmup_read_hold: ReadHold,
2553 read_policy: Option<ReadPolicy>,
2559
2560 storage_dependencies: BTreeMap<GlobalId, ReadHold>,
2563 compute_dependencies: BTreeMap<GlobalId, ReadHold>,
2566
2567 introspection: CollectionIntrospection,
2569
2570 wallclock_lag_histogram_stash: Option<
2577 BTreeMap<
2578 (
2579 WallclockLagHistogramPeriod,
2580 WallclockLag,
2581 BTreeMap<&'static str, String>,
2582 ),
2583 Diff,
2584 >,
2585 >,
2586}
2587
2588impl CollectionState {
2589 fn new(
2591 collection_id: GlobalId,
2592 as_of: Antichain<Timestamp>,
2593 shared: SharedCollectionState,
2594 storage_dependencies: BTreeMap<GlobalId, ReadHold>,
2595 compute_dependencies: BTreeMap<GlobalId, ReadHold>,
2596 read_hold_tx: read_holds::ChangeTx,
2597 introspection: CollectionIntrospection,
2598 ) -> Self {
2599 let since = as_of.clone();
2601 let upper = as_of;
2603
2604 assert!(shared.lock_read_capabilities(|c| c.frontier() == since.borrow()));
2606 assert!(shared.lock_write_frontier(|f| f == &upper));
2607
2608 let implied_read_hold =
2612 ReadHold::new(collection_id, since.clone(), Arc::clone(&read_hold_tx));
2613 let warmup_read_hold = ReadHold::new(collection_id, since.clone(), read_hold_tx);
2614
2615 let updates = warmup_read_hold.since().iter().map(|t| (t.clone(), 1));
2616 shared.lock_read_capabilities(|c| {
2617 c.update_iter(updates);
2618 });
2619
2620 let wallclock_lag_histogram_stash = match collection_id.is_transient() {
2624 true => None,
2625 false => Some(Default::default()),
2626 };
2627
2628 Self {
2629 target_replica: None,
2630 log_collection: false,
2631 dropped: false,
2632 scheduled: false,
2633 read_only: true,
2634 shared,
2635 implied_read_hold,
2636 warmup_read_hold,
2637 read_policy: Some(ReadPolicy::ValidFrom(since)),
2638 storage_dependencies,
2639 compute_dependencies,
2640 introspection,
2641 wallclock_lag_histogram_stash,
2642 }
2643 }
2644
2645 fn new_log_collection(
2647 id: GlobalId,
2648 shared: SharedCollectionState,
2649 read_hold_tx: read_holds::ChangeTx,
2650 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
2651 ) -> Self {
2652 let since = Antichain::from_elem(Timestamp::MIN);
2653 let introspection = CollectionIntrospection::new(
2654 id,
2655 introspection_tx,
2656 since.clone(),
2657 false,
2658 None,
2659 None,
2660 Vec::new(),
2661 );
2662 let mut state = Self::new(
2663 id,
2664 since,
2665 shared,
2666 Default::default(),
2667 Default::default(),
2668 read_hold_tx,
2669 introspection,
2670 );
2671 state.log_collection = true;
2672 state.scheduled = true;
2674 state
2675 }
2676
2677 fn read_frontier(&self) -> Antichain<Timestamp> {
2679 self.shared
2680 .lock_read_capabilities(|c| c.frontier().to_owned())
2681 }
2682
2683 fn write_frontier(&self) -> Antichain<Timestamp> {
2685 self.shared.lock_write_frontier(|f| f.clone())
2686 }
2687
2688 fn storage_dependency_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
2689 self.storage_dependencies.keys().copied()
2690 }
2691
2692 fn compute_dependency_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
2693 self.compute_dependencies.keys().copied()
2694 }
2695}
2696
2697#[derive(Clone, Debug)]
2708pub(super) struct SharedCollectionState {
2709 read_capabilities: Arc<Mutex<MutableAntichain<Timestamp>>>,
2722 write_frontier: Arc<Mutex<Antichain<Timestamp>>>,
2724}
2725
2726impl SharedCollectionState {
2727 pub fn new(as_of: Antichain<Timestamp>) -> Self {
2728 let since = as_of.clone();
2730 let upper = as_of;
2732
2733 let mut read_capabilities = MutableAntichain::new();
2737 read_capabilities.update_iter(since.iter().map(|time| (time.clone(), 1)));
2738
2739 Self {
2740 read_capabilities: Arc::new(Mutex::new(read_capabilities)),
2741 write_frontier: Arc::new(Mutex::new(upper)),
2742 }
2743 }
2744
2745 pub fn lock_read_capabilities<F, R>(&self, f: F) -> R
2746 where
2747 F: FnOnce(&mut MutableAntichain<Timestamp>) -> R,
2748 {
2749 let mut caps = self.read_capabilities.lock().expect("poisoned");
2750 f(&mut *caps)
2751 }
2752
2753 pub fn lock_write_frontier<F, R>(&self, f: F) -> R
2754 where
2755 F: FnOnce(&mut Antichain<Timestamp>) -> R,
2756 {
2757 let mut frontier = self.write_frontier.lock().expect("poisoned");
2758 f(&mut *frontier)
2759 }
2760}
2761
2762#[derive(Debug)]
2765struct CollectionIntrospection {
2766 collection_id: GlobalId,
2768 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
2770 frontiers: Option<FrontiersIntrospectionState>,
2775 refresh: Option<RefreshIntrospectionState>,
2779 dependency_ids: Vec<GlobalId>,
2781}
2782
2783impl CollectionIntrospection {
2784 fn new(
2785 collection_id: GlobalId,
2786 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
2787 as_of: Antichain<Timestamp>,
2788 storage_sink: bool,
2789 initial_as_of: Option<Antichain<Timestamp>>,
2790 refresh_schedule: Option<RefreshSchedule>,
2791 dependency_ids: Vec<GlobalId>,
2792 ) -> Self {
2793 let refresh =
2794 match (refresh_schedule, initial_as_of) {
2795 (Some(refresh_schedule), Some(initial_as_of)) => Some(
2796 RefreshIntrospectionState::new(refresh_schedule, initial_as_of, &as_of),
2797 ),
2798 (refresh_schedule, _) => {
2799 soft_assert_or_log!(
2802 refresh_schedule.is_none(),
2803 "`refresh_schedule` without an `initial_as_of`: {collection_id}"
2804 );
2805 None
2806 }
2807 };
2808 let frontiers = (!storage_sink).then(|| FrontiersIntrospectionState::new(as_of));
2809
2810 let self_ = Self {
2811 collection_id,
2812 introspection_tx,
2813 frontiers,
2814 refresh,
2815 dependency_ids,
2816 };
2817
2818 self_.report_initial_state();
2819 self_
2820 }
2821
2822 fn report_initial_state(&self) {
2824 if let Some(frontiers) = &self.frontiers {
2825 let row = frontiers.row_for_collection(self.collection_id);
2826 let updates = vec![(row, Diff::ONE)];
2827 self.send(IntrospectionType::Frontiers, updates);
2828 }
2829
2830 if let Some(refresh) = &self.refresh {
2831 let row = refresh.row_for_collection(self.collection_id);
2832 let updates = vec![(row, Diff::ONE)];
2833 self.send(IntrospectionType::ComputeMaterializedViewRefreshes, updates);
2834 }
2835
2836 if !self.dependency_ids.is_empty() {
2837 let updates = self.dependency_rows(Diff::ONE);
2838 self.send(IntrospectionType::ComputeDependencies, updates);
2839 }
2840 }
2841
2842 fn dependency_rows(&self, diff: Diff) -> Vec<(Row, Diff)> {
2844 self.dependency_ids
2845 .iter()
2846 .map(|dependency_id| {
2847 let row = Row::pack_slice(&[
2848 Datum::String(&self.collection_id.to_string()),
2849 Datum::String(&dependency_id.to_string()),
2850 ]);
2851 (row, diff)
2852 })
2853 .collect()
2854 }
2855
2856 fn observe_frontiers(
2859 &mut self,
2860 read_frontier: &Antichain<Timestamp>,
2861 write_frontier: &Antichain<Timestamp>,
2862 ) {
2863 self.update_frontier_introspection(read_frontier, write_frontier);
2864 self.update_refresh_introspection(write_frontier);
2865 }
2866
2867 fn update_frontier_introspection(
2868 &mut self,
2869 read_frontier: &Antichain<Timestamp>,
2870 write_frontier: &Antichain<Timestamp>,
2871 ) {
2872 let Some(frontiers) = &mut self.frontiers else {
2873 return;
2874 };
2875
2876 if &frontiers.read_frontier == read_frontier && &frontiers.write_frontier == write_frontier
2877 {
2878 return; };
2880
2881 let retraction = frontiers.row_for_collection(self.collection_id);
2882 frontiers.update(read_frontier, write_frontier);
2883 let insertion = frontiers.row_for_collection(self.collection_id);
2884 let updates = vec![(retraction, Diff::MINUS_ONE), (insertion, Diff::ONE)];
2885 self.send(IntrospectionType::Frontiers, updates);
2886 }
2887
2888 fn update_refresh_introspection(&mut self, write_frontier: &Antichain<Timestamp>) {
2889 let Some(refresh) = &mut self.refresh else {
2890 return;
2891 };
2892
2893 let retraction = refresh.row_for_collection(self.collection_id);
2894 refresh.frontier_update(write_frontier);
2895 let insertion = refresh.row_for_collection(self.collection_id);
2896
2897 if retraction == insertion {
2898 return; }
2900
2901 let updates = vec![(retraction, Diff::MINUS_ONE), (insertion, Diff::ONE)];
2902 self.send(IntrospectionType::ComputeMaterializedViewRefreshes, updates);
2903 }
2904
2905 fn send(&self, introspection_type: IntrospectionType, updates: Vec<(Row, Diff)>) {
2906 let _ = self.introspection_tx.send((introspection_type, updates));
2909 }
2910}
2911
2912impl Drop for CollectionIntrospection {
2913 fn drop(&mut self) {
2914 if let Some(frontiers) = &self.frontiers {
2916 let row = frontiers.row_for_collection(self.collection_id);
2917 let updates = vec![(row, Diff::MINUS_ONE)];
2918 self.send(IntrospectionType::Frontiers, updates);
2919 }
2920
2921 if let Some(refresh) = &self.refresh {
2923 let retraction = refresh.row_for_collection(self.collection_id);
2924 let updates = vec![(retraction, Diff::MINUS_ONE)];
2925 self.send(IntrospectionType::ComputeMaterializedViewRefreshes, updates);
2926 }
2927
2928 if !self.dependency_ids.is_empty() {
2930 let updates = self.dependency_rows(Diff::MINUS_ONE);
2931 self.send(IntrospectionType::ComputeDependencies, updates);
2932 }
2933 }
2934}
2935
2936#[derive(Debug)]
2937struct FrontiersIntrospectionState {
2938 read_frontier: Antichain<Timestamp>,
2939 write_frontier: Antichain<Timestamp>,
2940}
2941
2942impl FrontiersIntrospectionState {
2943 fn new(as_of: Antichain<Timestamp>) -> Self {
2944 Self {
2945 read_frontier: as_of.clone(),
2946 write_frontier: as_of,
2947 }
2948 }
2949
2950 fn row_for_collection(&self, collection_id: GlobalId) -> Row {
2952 let read_frontier = self
2953 .read_frontier
2954 .as_option()
2955 .map_or(Datum::Null, |ts| ts.clone().into());
2956 let write_frontier = self
2957 .write_frontier
2958 .as_option()
2959 .map_or(Datum::Null, |ts| ts.clone().into());
2960 Row::pack_slice(&[
2961 Datum::String(&collection_id.to_string()),
2962 read_frontier,
2963 write_frontier,
2964 ])
2965 }
2966
2967 fn update(
2969 &mut self,
2970 read_frontier: &Antichain<Timestamp>,
2971 write_frontier: &Antichain<Timestamp>,
2972 ) {
2973 if read_frontier != &self.read_frontier {
2974 self.read_frontier.clone_from(read_frontier);
2975 }
2976 if write_frontier != &self.write_frontier {
2977 self.write_frontier.clone_from(write_frontier);
2978 }
2979 }
2980}
2981
2982#[derive(Debug)]
2985struct RefreshIntrospectionState {
2986 refresh_schedule: RefreshSchedule,
2988 initial_as_of: Antichain<Timestamp>,
2989 next_refresh: Datum<'static>, last_completed_refresh: Datum<'static>, }
2993
2994impl RefreshIntrospectionState {
2995 fn row_for_collection(&self, collection_id: GlobalId) -> Row {
2997 Row::pack_slice(&[
2998 Datum::String(&collection_id.to_string()),
2999 self.last_completed_refresh,
3000 self.next_refresh,
3001 ])
3002 }
3003}
3004
3005impl RefreshIntrospectionState {
3006 fn new(
3009 refresh_schedule: RefreshSchedule,
3010 initial_as_of: Antichain<Timestamp>,
3011 upper: &Antichain<Timestamp>,
3012 ) -> Self {
3013 let mut self_ = Self {
3014 refresh_schedule: refresh_schedule.clone(),
3015 initial_as_of: initial_as_of.clone(),
3016 next_refresh: Datum::Null,
3017 last_completed_refresh: Datum::Null,
3018 };
3019 self_.frontier_update(upper);
3020 self_
3021 }
3022
3023 fn frontier_update(&mut self, write_frontier: &Antichain<Timestamp>) {
3026 if write_frontier.is_empty() {
3027 self.last_completed_refresh =
3028 if let Some(last_refresh) = self.refresh_schedule.last_refresh() {
3029 last_refresh.into()
3030 } else {
3031 Timestamp::MAX.into()
3034 };
3035 self.next_refresh = Datum::Null;
3036 } else {
3037 if PartialOrder::less_equal(write_frontier, &self.initial_as_of) {
3038 self.last_completed_refresh = Datum::Null;
3040 let initial_as_of = self.initial_as_of.as_option().expect(
3041 "initial_as_of can't be [], because then there would be no refreshes at all",
3042 );
3043 let first_refresh = self
3044 .refresh_schedule
3045 .round_up_timestamp(*initial_as_of)
3046 .expect("sequencing makes sure that REFRESH MVs always have a first refresh");
3047 soft_assert_or_log!(
3048 first_refresh == *initial_as_of,
3049 "initial_as_of should be set to the first refresh"
3050 );
3051 self.next_refresh = first_refresh.into();
3052 } else {
3053 let write_frontier = write_frontier.as_option().expect("checked above");
3055 self.last_completed_refresh = self
3056 .refresh_schedule
3057 .round_down_timestamp_m1(*write_frontier)
3058 .map_or_else(
3059 || {
3060 soft_panic_or_log!(
3061 "rounding down should have returned the first refresh or later"
3062 );
3063 Datum::Null
3064 },
3065 |last_completed_refresh| last_completed_refresh.into(),
3066 );
3067 self.next_refresh = write_frontier.clone().into();
3068 }
3069 }
3070 }
3071}
3072
3073#[derive(Debug)]
3075struct PendingPeek {
3076 target_replica: Option<ReplicaId>,
3080 otel_ctx: OpenTelemetryContext,
3082 requested_at: Instant,
3086 read_hold: ReadHold,
3088 peek_response_tx: oneshot::Sender<PeekResponse>,
3090 limit: Option<usize>,
3092 offset: usize,
3094}
3095
3096#[derive(Debug, Clone)]
3097struct ActiveSubscribe {
3098 frontier: Antichain<Timestamp>,
3100}
3101
3102impl Default for ActiveSubscribe {
3103 fn default() -> Self {
3104 Self {
3105 frontier: Antichain::from_elem(Timestamp::MIN),
3106 }
3107 }
3108}
3109
3110#[derive(Debug)]
3112struct ReplicaState {
3113 id: ReplicaId,
3115 client: ReplicaClient,
3117 config: ReplicaConfig,
3119 metrics: ReplicaMetrics,
3121 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3123 collections: BTreeMap<GlobalId, ReplicaCollectionState>,
3125 epoch: u64,
3127 dyncfg_override: ConfigUpdates,
3131}
3132
3133impl ReplicaState {
3134 fn new(
3135 id: ReplicaId,
3136 client: ReplicaClient,
3137 config: ReplicaConfig,
3138 metrics: ReplicaMetrics,
3139 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3140 epoch: u64,
3141 dyncfg_override: ConfigUpdates,
3142 ) -> Self {
3143 Self {
3144 id,
3145 client,
3146 config,
3147 metrics,
3148 introspection_tx,
3149 epoch,
3150 dyncfg_override,
3151 collections: Default::default(),
3152 }
3153 }
3154
3155 fn add_collection(
3161 &mut self,
3162 id: GlobalId,
3163 as_of: Antichain<Timestamp>,
3164 input_read_holds: Vec<ReadHold>,
3165 ) {
3166 let metrics = self.metrics.for_collection(id);
3167 let introspection = ReplicaCollectionIntrospection::new(
3168 self.id,
3169 id,
3170 self.introspection_tx.clone(),
3171 as_of.clone(),
3172 );
3173 let mut state =
3174 ReplicaCollectionState::new(metrics, as_of, introspection, input_read_holds);
3175
3176 if id.is_transient() {
3180 state.wallclock_lag_max = None;
3181 }
3182
3183 if let Some(previous) = self.collections.insert(id, state) {
3184 panic!("attempt to add a collection with existing ID {id} (previous={previous:?}");
3185 }
3186 }
3187
3188 fn remove_collection(&mut self, id: GlobalId) -> Option<ReplicaCollectionState> {
3190 self.collections.remove(&id)
3191 }
3192
3193 fn expect_collection(&self, id: GlobalId) -> &ReplicaCollectionState {
3200 self.collections
3201 .get(&id)
3202 .expect("hosting replica must have per-replica collection state")
3203 }
3204
3205 fn collection_frontiers_empty(&self, id: GlobalId) -> bool {
3207 self.collections.get(&id).map_or(true, |c| {
3208 c.write_frontier.is_empty()
3209 && c.input_frontier.is_empty()
3210 && c.output_frontier.is_empty()
3211 })
3212 }
3213
3214 #[mz_ore::instrument(level = "debug")]
3218 pub fn dump(&self) -> Result<serde_json::Value, anyhow::Error> {
3219 let Self {
3226 id,
3227 client: _,
3228 config: _,
3229 metrics: _,
3230 introspection_tx: _,
3231 epoch,
3232 dyncfg_override,
3233 collections,
3234 } = self;
3235
3236 let collections: BTreeMap<_, _> = collections
3237 .iter()
3238 .map(|(id, collection)| (id.to_string(), format!("{collection:?}")))
3239 .collect();
3240
3241 Ok(serde_json::json!({
3242 "id": id.to_string(),
3243 "collections": collections,
3244 "epoch": epoch,
3245 "dyncfg_override": format!("{dyncfg_override:?}"),
3246 }))
3247 }
3248}
3249
3250#[derive(Debug)]
3251struct ReplicaCollectionState {
3252 write_frontier: Antichain<Timestamp>,
3256 input_frontier: Antichain<Timestamp>,
3260 output_frontier: Antichain<Timestamp>,
3264
3265 metrics: Option<ReplicaCollectionMetrics>,
3269 as_of: Antichain<Timestamp>,
3271 introspection: ReplicaCollectionIntrospection,
3273 input_read_holds: Vec<ReadHold>,
3279
3280 wallclock_lag_max: Option<WallclockLag>,
3284}
3285
3286impl ReplicaCollectionState {
3287 fn new(
3288 metrics: Option<ReplicaCollectionMetrics>,
3289 as_of: Antichain<Timestamp>,
3290 introspection: ReplicaCollectionIntrospection,
3291 input_read_holds: Vec<ReadHold>,
3292 ) -> Self {
3293 Self {
3294 write_frontier: as_of.clone(),
3295 input_frontier: as_of.clone(),
3296 output_frontier: as_of.clone(),
3297 metrics,
3298 as_of,
3299 introspection,
3300 input_read_holds,
3301 wallclock_lag_max: Some(WallclockLag::MIN),
3302 }
3303 }
3304
3305 fn hydrated(&self) -> bool {
3307 self.as_of.is_empty() || PartialOrder::less_than(&self.as_of, &self.output_frontier)
3323 }
3324
3325 fn update_write_frontier(&mut self, new_frontier: Antichain<Timestamp>) {
3327 if PartialOrder::less_than(&new_frontier, &self.write_frontier) {
3328 soft_panic_or_log!(
3329 "replica collection write frontier regression (old={:?}, new={new_frontier:?})",
3330 self.write_frontier,
3331 );
3332 return;
3333 } else if new_frontier == self.write_frontier {
3334 return;
3335 }
3336
3337 self.write_frontier = new_frontier;
3338 }
3339
3340 fn update_input_frontier(&mut self, new_frontier: Antichain<Timestamp>) {
3342 if PartialOrder::less_than(&new_frontier, &self.input_frontier) {
3343 soft_panic_or_log!(
3344 "replica collection input frontier regression (old={:?}, new={new_frontier:?})",
3345 self.input_frontier,
3346 );
3347 return;
3348 } else if new_frontier == self.input_frontier {
3349 return;
3350 }
3351
3352 self.input_frontier = new_frontier;
3353
3354 for read_hold in &mut self.input_read_holds {
3356 let result = read_hold.try_downgrade(self.input_frontier.clone());
3357 soft_assert_or_log!(
3358 result.is_ok(),
3359 "read hold downgrade failed (read_hold={read_hold:?}, new_since={:?})",
3360 self.input_frontier,
3361 );
3362 }
3363 }
3364
3365 fn update_output_frontier(&mut self, new_frontier: Antichain<Timestamp>) {
3367 if PartialOrder::less_than(&new_frontier, &self.output_frontier) {
3368 soft_panic_or_log!(
3369 "replica collection output frontier regression (old={:?}, new={new_frontier:?})",
3370 self.output_frontier,
3371 );
3372 return;
3373 } else if new_frontier == self.output_frontier {
3374 return;
3375 }
3376
3377 self.output_frontier = new_frontier;
3378 }
3379}
3380
3381#[derive(Debug)]
3384struct ReplicaCollectionIntrospection {
3385 replica_id: ReplicaId,
3387 collection_id: GlobalId,
3389 write_frontier: Antichain<Timestamp>,
3391 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3393}
3394
3395impl ReplicaCollectionIntrospection {
3396 fn new(
3398 replica_id: ReplicaId,
3399 collection_id: GlobalId,
3400 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3401 as_of: Antichain<Timestamp>,
3402 ) -> Self {
3403 let self_ = Self {
3404 replica_id,
3405 collection_id,
3406 write_frontier: as_of,
3407 introspection_tx,
3408 };
3409
3410 self_.report_initial_state();
3411 self_
3412 }
3413
3414 fn report_initial_state(&self) {
3416 let row = self.write_frontier_row();
3417 let updates = vec![(row, Diff::ONE)];
3418 self.send(IntrospectionType::ReplicaFrontiers, updates);
3419 }
3420
3421 fn observe_frontier(&mut self, write_frontier: &Antichain<Timestamp>) {
3423 if self.write_frontier == *write_frontier {
3424 return; }
3426
3427 let retraction = self.write_frontier_row();
3428 self.write_frontier.clone_from(write_frontier);
3429 let insertion = self.write_frontier_row();
3430
3431 let updates = vec![(retraction, Diff::MINUS_ONE), (insertion, Diff::ONE)];
3432 self.send(IntrospectionType::ReplicaFrontiers, updates);
3433 }
3434
3435 fn write_frontier_row(&self) -> Row {
3437 let write_frontier = self
3438 .write_frontier
3439 .as_option()
3440 .map_or(Datum::Null, |ts| ts.clone().into());
3441 Row::pack_slice(&[
3442 Datum::String(&self.collection_id.to_string()),
3443 Datum::String(&self.replica_id.to_string()),
3444 write_frontier,
3445 ])
3446 }
3447
3448 fn send(&self, introspection_type: IntrospectionType, updates: Vec<(Row, Diff)>) {
3449 let _ = self.introspection_tx.send((introspection_type, updates));
3452 }
3453}
3454
3455impl Drop for ReplicaCollectionIntrospection {
3456 fn drop(&mut self) {
3457 let row = self.write_frontier_row();
3459 let updates = vec![(row, Diff::MINUS_ONE)];
3460 self.send(IntrospectionType::ReplicaFrontiers, updates);
3461 }
3462}
3463
3464fn classify_collection_readiness<'a, I>(
3470 replicas: I,
3471 target_replica_ids: &BTreeSet<ReplicaId>,
3472 reference_replica_ids: &BTreeSet<ReplicaId>,
3473 allowed_lag: Option<Timestamp>,
3474) -> CollectionReadiness
3475where
3476 I: Iterator<Item = (ReplicaId, &'a ReplicaCollectionState)> + Clone,
3477{
3478 let lag = allowed_lag.map(|allowed_lag| {
3479 let mut reference = Antichain::from_elem(Timestamp::MIN);
3480 for (_, state) in replicas
3481 .clone()
3482 .filter(|(id, _)| reference_replica_ids.contains(id))
3483 {
3484 reference.join_assign(&state.output_frontier);
3485 }
3486 (reference, allowed_lag)
3487 });
3488
3489 let mut result = CollectionReadiness::Unhydrated;
3490 for (_, state) in replicas.filter(|(id, _)| target_replica_ids.contains(id)) {
3491 match CollectionReadiness::classify(
3492 state.hydrated(),
3493 &state.output_frontier,
3494 lag.as_ref().map(|(reference, lag)| (reference, *lag)),
3495 ) {
3496 CollectionReadiness::Ready => return CollectionReadiness::Ready,
3497 CollectionReadiness::Lagging { lag } => {
3498 let lag = match result {
3501 CollectionReadiness::Lagging {
3502 lag: Some(previous),
3503 } => Some(previous.min(lag.unwrap_or(u64::MAX))),
3504 _ => lag,
3505 };
3506 result = CollectionReadiness::Lagging { lag };
3507 }
3508 CollectionReadiness::Unhydrated => {}
3509 }
3510 }
3511 result
3512}
3513
3514#[cfg(test)]
3515mod tests {
3516 use std::collections::BTreeSet;
3517
3518 use mz_compute_types::dyncfgs::{ENABLE_COLUMN_PAGED_BATCHER, ENABLE_MZ_JOIN_CORE};
3519 use mz_dyncfg::{ConfigSet, ConfigUpdates, ConfigVal};
3520 use mz_persist_types::PersistLocation;
3521 use mz_repr::{GlobalId, Timestamp};
3522 use timely::progress::Antichain;
3523 use tokio::sync::mpsc;
3524
3525 use crate::protocol::command::{ComputeCommand, InstanceConfig};
3526
3527 use super::{
3528 CollectionReadiness, Instance, ReplicaCollectionIntrospection, ReplicaCollectionState,
3529 ReplicaId, classify_collection_readiness,
3530 };
3531
3532 fn ac(ts: u64) -> Antichain<Timestamp> {
3533 Antichain::from_elem(Timestamp::new(ts))
3534 }
3535
3536 fn state(id: ReplicaId, as_of: u64, write: u64, output: u64) -> ReplicaCollectionState {
3537 let (tx, _rx) = mpsc::unbounded_channel();
3538 let introspection =
3539 ReplicaCollectionIntrospection::new(id, GlobalId::User(1), tx, ac(as_of));
3540 let mut state = ReplicaCollectionState::new(None, ac(as_of), introspection, Vec::new());
3541 state.update_write_frontier(ac(write));
3542 state.update_output_frontier(ac(output));
3543 state
3544 }
3545
3546 fn classify(
3547 replicas: &[(ReplicaId, ReplicaCollectionState)],
3548 targets: &[ReplicaId],
3549 references: &[ReplicaId],
3550 lag: Option<Timestamp>,
3551 ) -> CollectionReadiness {
3552 classify_collection_readiness(
3553 replicas.iter().map(|(id, state)| (*id, state)),
3554 &targets.iter().copied().collect::<BTreeSet<_>>(),
3555 &references.iter().copied().collect::<BTreeSet<_>>(),
3556 lag,
3557 )
3558 }
3559
3560 const LAG: Option<Timestamp> = Some(Timestamp::new(60));
3561
3562 #[mz_ore::test]
3566 fn shared_write_does_not_hide_output_lag() {
3567 let reference = ReplicaId::User(1);
3568 let target = ReplicaId::User(2);
3569 let mut replicas = vec![
3570 (reference, state(reference, 100, 10_000, 10_000)),
3571 (target, state(target, 100, 10_000, 101)),
3574 ];
3575 assert_eq!(
3576 classify(&replicas, &[target], &[reference], LAG),
3577 CollectionReadiness::Lagging { lag: Some(9_899) },
3578 );
3579 replicas[1].1.update_output_frontier(ac(9_940));
3580 assert_eq!(
3581 classify(&replicas, &[target], &[reference], LAG),
3582 CollectionReadiness::Ready,
3583 );
3584 }
3585
3586 #[mz_ore::test]
3587 fn refresh_writes_ahead_of_outputs_is_ready() {
3588 let reference = ReplicaId::User(1);
3589 let target = ReplicaId::User(2);
3590 let replicas = vec![
3591 (reference, state(reference, 100, 20_000, 10_000)),
3592 (target, state(target, 100, 20_000, 10_000)),
3593 ];
3594 assert_eq!(
3595 classify(&replicas, &[target], &[reference], LAG),
3596 CollectionReadiness::Ready,
3597 );
3598 }
3599
3600 #[mz_ore::test]
3601 fn furthest_reference_and_bystander_exclusion() {
3602 let trailing = ReplicaId::User(1);
3603 let ahead = ReplicaId::User(2);
3604 let target = ReplicaId::User(3);
3605 let bystander = ReplicaId::User(4);
3606 let replicas = vec![
3607 (trailing, state(trailing, 100, 9_000, 9_000)),
3608 (ahead, state(ahead, 100, 10_000, 10_000)),
3609 (target, state(target, 100, 8_990, 8_990)),
3610 (bystander, state(bystander, 100, 1_000_000, 1_000_000)),
3611 ];
3612 assert_eq!(
3613 classify(&replicas, &[target], &[trailing], LAG),
3614 CollectionReadiness::Ready,
3615 );
3616 assert_eq!(
3617 classify(&replicas, &[target], &[trailing, ahead], LAG),
3618 CollectionReadiness::Lagging { lag: Some(1_010) },
3619 );
3620 }
3621
3622 #[mz_ore::test]
3623 fn one_ready_target_suffices() {
3624 let reference = ReplicaId::User(1);
3625 let unhydrated = ReplicaId::User(2);
3626 let lagging = ReplicaId::User(3);
3627 let ready = ReplicaId::User(4);
3628 let replicas = vec![
3629 (reference, state(reference, 100, 10_000, 10_000)),
3630 (unhydrated, state(unhydrated, 10_000, 10_000, 10_000)),
3634 (lagging, state(lagging, 100, 1_000, 1_000)),
3635 (ready, state(ready, 100, 9_990, 9_990)),
3636 ];
3637 assert_eq!(
3638 classify(&replicas, &[unhydrated, lagging, ready], &[reference], LAG),
3639 CollectionReadiness::Ready,
3640 );
3641 assert_eq!(
3642 classify(&replicas, &[unhydrated, lagging], &[reference], LAG),
3643 CollectionReadiness::Lagging { lag: Some(9_000) },
3644 );
3645 }
3646
3647 #[mz_ore::test]
3648 fn no_lag_gate_is_hydration_only() {
3649 let reference = ReplicaId::User(1);
3650 let target = ReplicaId::User(2);
3651 let replicas = vec![
3652 (reference, state(reference, 100, 10_000, 10_000)),
3653 (target, state(target, 100, 101, 101)),
3654 ];
3655 assert_eq!(
3656 classify(&replicas, &[target], &[reference], None),
3657 CollectionReadiness::Ready,
3658 );
3659 let unhydrated = vec![(target, state(target, 100, 100, 100))];
3660 assert_eq!(
3661 classify(&unhydrated, &[target], &[], None),
3662 CollectionReadiness::Unhydrated,
3663 );
3664 }
3665
3666 #[mz_ore::test]
3667 fn lag_reports_the_closest_hydrated_target() {
3668 let reference = ReplicaId::User(1);
3669 let far = ReplicaId::User(2);
3670 let close = ReplicaId::User(3);
3671 let mut replicas = vec![
3672 (reference, state(reference, 100, 10_000, 10_000)),
3673 (far, state(far, 100, 1_000, 1_000)),
3674 (close, state(close, 100, 9_000, 9_000)),
3675 ];
3676 for _ in 0..2 {
3677 assert_eq!(
3678 classify(&replicas, &[far, close], &[reference], LAG),
3679 CollectionReadiness::Lagging { lag: Some(1_000) },
3680 );
3681 replicas.reverse();
3682 }
3683 }
3684
3685 #[mz_ore::test]
3686 fn completed_reference_requires_completed_target() {
3687 let reference = ReplicaId::User(1);
3688 let target = ReplicaId::User(2);
3689 let mut replicas = vec![
3690 (reference, state(reference, 100, 1_000, 1_000)),
3691 (target, state(target, 100, 1_000, 1_000)),
3692 ];
3693 replicas[0].1.update_output_frontier(Antichain::new());
3694 assert_eq!(
3695 classify(&replicas, &[target], &[reference], Some(Timestamp::new(0))),
3696 CollectionReadiness::Lagging { lag: None },
3697 );
3698 replicas[1].1.update_output_frontier(Antichain::new());
3699 assert_eq!(
3700 classify(&replicas, &[target], &[reference], Some(Timestamp::new(0))),
3701 CollectionReadiness::Ready,
3702 );
3703 }
3704
3705 #[mz_ore::test]
3706 fn no_reference_means_nothing_to_regress() {
3707 let target = ReplicaId::User(1);
3708 let replicas = vec![(target, state(target, 100, 5_000, 5_000))];
3709 assert_eq!(
3710 classify(&replicas, &[target], &[], Some(Timestamp::new(0))),
3711 CollectionReadiness::Ready,
3712 );
3713 assert_eq!(
3714 classify(&replicas, &[target], &[target], Some(Timestamp::new(0))),
3715 CollectionReadiness::Ready,
3716 );
3717 }
3718
3719 #[mz_ore::test]
3720 fn no_targets_is_unhydrated() {
3721 let reference = ReplicaId::User(1);
3722 let replicas = vec![(reference, state(reference, 100, 10_000, 10_000))];
3723 assert_eq!(
3724 classify(&replicas, &[], &[reference], LAG),
3725 CollectionReadiness::Unhydrated,
3726 );
3727 }
3728
3729 fn create_instance_command() -> ComputeCommand {
3730 ComputeCommand::CreateInstance(Box::new(InstanceConfig {
3731 logging: Default::default(),
3732 expiration_offset: None,
3733 peek_stash_persist_location: PersistLocation::new_in_mem(),
3734 arrangement_dictionary_compression: false,
3735 initial_config: Default::default(),
3736 }))
3737 }
3738
3739 fn initial_config(cmd: &ComputeCommand) -> &ConfigUpdates {
3740 match cmd {
3741 ComputeCommand::CreateInstance(config) => &config.initial_config,
3742 other => panic!("expected CreateInstance, got {other:?}"),
3743 }
3744 }
3745
3746 #[mz_ore::test]
3751 fn create_instance_snapshots_instance_wide_dyncfg() {
3752 let dyncfg = ConfigSet::default()
3753 .add(&ENABLE_COLUMN_PAGED_BATCHER)
3754 .add(&ENABLE_MZ_JOIN_CORE);
3755 let mut updates = ConfigUpdates::default();
3756 updates.add(&ENABLE_COLUMN_PAGED_BATCHER, true);
3757 updates.add(&ENABLE_MZ_JOIN_CORE, false);
3758 updates.apply(&dyncfg);
3759
3760 let cmd = Instance::specialize_command_for_replica(
3762 create_instance_command(),
3763 &ConfigUpdates::default(),
3764 &dyncfg,
3765 );
3766 let snapshot = initial_config(&cmd);
3767 assert_eq!(
3768 snapshot.updates.get(ENABLE_COLUMN_PAGED_BATCHER.name()),
3769 Some(&ConfigVal::Bool(true)),
3770 );
3771 assert_eq!(
3772 snapshot.updates.get(ENABLE_MZ_JOIN_CORE.name()),
3773 Some(&ConfigVal::Bool(false)),
3774 );
3775 }
3776
3777 #[mz_ore::test]
3780 fn create_instance_snapshot_applies_replica_override() {
3781 let dyncfg = ConfigSet::default().add(&ENABLE_COLUMN_PAGED_BATCHER);
3782 let mut updates = ConfigUpdates::default();
3783 updates.add(&ENABLE_COLUMN_PAGED_BATCHER, true);
3784 updates.apply(&dyncfg);
3785
3786 let mut override_updates = ConfigUpdates::default();
3787 override_updates.add(&ENABLE_COLUMN_PAGED_BATCHER, false);
3788
3789 let cmd = Instance::specialize_command_for_replica(
3790 create_instance_command(),
3791 &override_updates,
3792 &dyncfg,
3793 );
3794 assert_eq!(
3795 initial_config(&cmd)
3796 .updates
3797 .get(ENABLE_COLUMN_PAGED_BATCHER.name()),
3798 Some(&ConfigVal::Bool(false)),
3799 "replica override should win over the instance-wide value",
3800 );
3801 }
3802
3803 #[mz_ore::test]
3805 fn update_configuration_merges_replica_override() {
3806 let dyncfg = ConfigSet::default().add(&ENABLE_COLUMN_PAGED_BATCHER);
3807
3808 let mut override_updates = ConfigUpdates::default();
3809 override_updates.add(&ENABLE_COLUMN_PAGED_BATCHER, true);
3810
3811 let cmd = Instance::specialize_command_for_replica(
3812 ComputeCommand::UpdateConfiguration(Box::new(Default::default())),
3813 &override_updates,
3814 &dyncfg,
3815 );
3816 match cmd {
3817 ComputeCommand::UpdateConfiguration(params) => assert_eq!(
3818 params
3819 .dyncfg_updates
3820 .updates
3821 .get(ENABLE_COLUMN_PAGED_BATCHER.name()),
3822 Some(&ConfigVal::Bool(true)),
3823 ),
3824 other => panic!("expected UpdateConfiguration, got {other:?}"),
3825 }
3826 }
3827}