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 Ok(())
1380 }
1381
1382 fn rehydrate_replica(&mut self, id: ReplicaId) {
1388 let replica = &self.replicas[&id];
1389 let config = replica.config.clone();
1390 let epoch = replica.epoch + 1;
1391 let dyncfg_override = replica.dyncfg_override.clone();
1392
1393 self.remove_replica(id).expect("replica must exist");
1400 let result = self.add_replica(id, config, Some(epoch), dyncfg_override);
1401
1402 match result {
1403 Ok(()) => (),
1404 Err(ReplicaExists(_)) => unreachable!("replica was removed"),
1405 }
1406 }
1407
1408 fn rehydrate_failed_replicas(&mut self) {
1410 let replicas = self.replicas.iter();
1411 let failed_replicas: Vec<_> = replicas
1412 .filter_map(|(id, replica)| replica.client.is_failed().then_some(*id))
1413 .collect();
1414
1415 for replica_id in failed_replicas {
1416 self.rehydrate_replica(replica_id);
1417 }
1418 }
1419
1420 #[mz_ore::instrument(level = "debug")]
1425 pub fn create_dataflow(
1426 &mut self,
1427 dataflow: LirDataflowDescription,
1428 import_read_holds: Vec<ReadHold>,
1429 mut shared_collection_state: BTreeMap<GlobalId, SharedCollectionState>,
1430 target_replica: Option<ReplicaId>,
1431 ) -> Result<(), DataflowCreationError> {
1432 use DataflowCreationError::*;
1433
1434 if let Some(replica_id) = target_replica {
1438 if !self.replica_exists(replica_id) {
1439 return Err(ReplicaMissing(replica_id));
1440 }
1441 }
1442
1443 let as_of = dataflow.as_of.as_ref().ok_or(MissingAsOf)?;
1445 if as_of.is_empty() && dataflow.subscribe_ids().next().is_some() {
1446 return Err(EmptyAsOfForSubscribe);
1447 }
1448 if as_of.is_empty() && dataflow.copy_to_ids().next().is_some() {
1449 return Err(EmptyAsOfForCopyTo);
1450 }
1451
1452 let mut storage_dependencies = BTreeMap::new();
1454 let mut compute_dependencies = BTreeMap::new();
1455
1456 let mut replica_input_read_holds = Vec::new();
1461
1462 let mut import_read_holds: BTreeMap<_, _> =
1463 import_read_holds.into_iter().map(|r| (r.id(), r)).collect();
1464
1465 for &id in dataflow.source_imports.keys() {
1466 let mut read_hold = import_read_holds.remove(&id).ok_or(ReadHoldMissing(id))?;
1467 replica_input_read_holds.push(read_hold.clone());
1468
1469 read_hold
1470 .try_downgrade(as_of.clone())
1471 .map_err(|_| ReadHoldInsufficient(id))?;
1472 storage_dependencies.insert(id, read_hold);
1473 }
1474
1475 for &id in dataflow.index_imports.keys() {
1476 let mut read_hold = import_read_holds.remove(&id).ok_or(ReadHoldMissing(id))?;
1477 read_hold
1478 .try_downgrade(as_of.clone())
1479 .map_err(|_| ReadHoldInsufficient(id))?;
1480 compute_dependencies.insert(id, read_hold);
1481 }
1482
1483 if as_of.is_empty() {
1486 replica_input_read_holds = Default::default();
1487 }
1488
1489 for export_id in dataflow.export_ids() {
1491 let shared = shared_collection_state
1492 .remove(&export_id)
1493 .unwrap_or_else(|| SharedCollectionState::new(as_of.clone()));
1494 let write_only = dataflow.sink_exports.contains_key(&export_id);
1495 let storage_sink = dataflow.persist_sink_ids().any(|id| id == export_id);
1496
1497 self.add_collection(
1498 export_id,
1499 as_of.clone(),
1500 shared,
1501 storage_dependencies.clone(),
1502 compute_dependencies.clone(),
1503 replica_input_read_holds.clone(),
1504 write_only,
1505 storage_sink,
1506 dataflow.initial_storage_as_of.clone(),
1507 dataflow.refresh_schedule.clone(),
1508 target_replica,
1509 );
1510
1511 if let Ok(frontiers) = self.storage_collections.collection_frontiers(export_id) {
1514 self.maybe_update_global_write_frontier(export_id, frontiers.write_frontier);
1515 }
1516 }
1517
1518 for subscribe_id in dataflow.subscribe_ids() {
1520 self.subscribes
1521 .insert(subscribe_id, ActiveSubscribe::default());
1522 }
1523
1524 for copy_to_id in dataflow.copy_to_ids() {
1526 self.copy_tos.insert(copy_to_id);
1527 }
1528
1529 let mut source_imports = BTreeMap::new();
1532 for (id, import) in dataflow.source_imports {
1533 let frontiers = self
1534 .storage_collections
1535 .collection_frontiers(id)
1536 .expect("collection exists");
1537
1538 let collection_metadata = self
1539 .storage_collections
1540 .collection_metadata(id)
1541 .expect("we have a read hold on this collection");
1542
1543 let desc = SourceInstanceDesc {
1544 storage_metadata: collection_metadata.clone(),
1545 arguments: import.desc.arguments,
1546 typ: import.desc.typ.clone(),
1547 };
1548 source_imports.insert(
1549 id,
1550 mz_compute_types::dataflows::SourceImport {
1551 desc,
1552 monotonic: import.monotonic,
1553 with_snapshot: import.with_snapshot,
1554 upper: frontiers.write_frontier,
1555 },
1556 );
1557 }
1558
1559 let mut sink_exports = BTreeMap::new();
1560 for (id, se) in dataflow.sink_exports {
1561 let connection = match se.connection {
1562 ComputeSinkConnection::MaterializedView(conn) => {
1563 let metadata = self
1564 .storage_collections
1565 .collection_metadata(id)
1566 .map_err(|_| CollectionMissing(id))?
1567 .clone();
1568 let conn = MaterializedViewSinkConnection {
1569 value_desc: conn.value_desc,
1570 storage_metadata: metadata,
1571 };
1572 ComputeSinkConnection::MaterializedView(conn)
1573 }
1574 ComputeSinkConnection::Subscribe(conn) => ComputeSinkConnection::Subscribe(conn),
1575 ComputeSinkConnection::CopyToS3Oneshot(conn) => {
1576 ComputeSinkConnection::CopyToS3Oneshot(conn)
1577 }
1578 ComputeSinkConnection::MetricSink(conn) => ComputeSinkConnection::MetricSink(conn),
1579 };
1580 let desc = ComputeSinkDesc {
1581 from: se.from,
1582 from_desc: se.from_desc,
1583 connection,
1584 with_snapshot: se.with_snapshot,
1585 up_to: se.up_to,
1586 non_null_assertions: se.non_null_assertions,
1587 refresh_schedule: se.refresh_schedule,
1588 from_arrangement: se.from_arrangement,
1589 };
1590 sink_exports.insert(id, desc);
1591 }
1592
1593 let objects_to_build = dataflow
1595 .objects_to_build
1596 .into_iter()
1597 .map(|object| BuildDesc {
1598 id: object.id,
1599 plan: RenderPlan::try_from(object.plan).expect("valid plan"),
1600 })
1601 .collect();
1602
1603 let augmented_dataflow = DataflowDescription {
1604 source_imports,
1605 sink_exports,
1606 objects_to_build,
1607 index_imports: dataflow.index_imports,
1609 index_exports: dataflow.index_exports,
1610 as_of: dataflow.as_of.clone(),
1611 until: dataflow.until,
1612 initial_storage_as_of: dataflow.initial_storage_as_of,
1613 refresh_schedule: dataflow.refresh_schedule,
1614 debug_name: dataflow.debug_name,
1615 time_dependence: dataflow.time_dependence,
1616 };
1617
1618 if augmented_dataflow.is_transient() {
1619 tracing::debug!(
1620 name = %augmented_dataflow.debug_name,
1621 import_ids = %augmented_dataflow.display_import_ids(),
1622 export_ids = %augmented_dataflow.display_export_ids(),
1623 as_of = ?augmented_dataflow.as_of.as_ref().unwrap().elements(),
1624 until = ?augmented_dataflow.until.elements(),
1625 "creating dataflow",
1626 );
1627 } else {
1628 tracing::info!(
1629 name = %augmented_dataflow.debug_name,
1630 import_ids = %augmented_dataflow.display_import_ids(),
1631 export_ids = %augmented_dataflow.display_export_ids(),
1632 as_of = ?augmented_dataflow.as_of.as_ref().unwrap().elements(),
1633 until = ?augmented_dataflow.until.elements(),
1634 "creating dataflow",
1635 );
1636 }
1637
1638 if as_of.is_empty() {
1641 tracing::info!(
1642 name = %augmented_dataflow.debug_name,
1643 "not sending `CreateDataflow`, because of empty `as_of`",
1644 );
1645 } else {
1646 let collections: Vec<_> = augmented_dataflow.export_ids().collect();
1647 self.send(ComputeCommand::CreateDataflow(Box::new(augmented_dataflow)));
1648
1649 for id in collections {
1650 self.maybe_schedule_collection(id);
1651 }
1652 }
1653
1654 Ok(())
1655 }
1656
1657 fn maybe_schedule_collection(&mut self, id: GlobalId) {
1663 let collection = self.expect_collection(id);
1664
1665 if collection.scheduled {
1667 return;
1668 }
1669
1670 let as_of = collection.read_frontier();
1671
1672 if as_of.is_empty() {
1675 return;
1676 }
1677
1678 let ready = if id.is_transient() {
1679 true
1685 } else {
1686 let not_self_dep = |x: &GlobalId| *x != id;
1692
1693 let mut deps_scheduled = true;
1696
1697 let compute_deps = collection.compute_dependency_ids().filter(not_self_dep);
1702 let mut compute_frontiers = Vec::new();
1703 for id in compute_deps {
1704 let dep = &self.expect_collection(id);
1705 deps_scheduled &= dep.scheduled;
1706 compute_frontiers.push(dep.write_frontier());
1707 }
1708
1709 let storage_deps = collection.storage_dependency_ids().filter(not_self_dep);
1710 let storage_frontiers = self
1711 .storage_collections
1712 .collections_frontiers(storage_deps.collect())
1713 .expect("must exist");
1714 let storage_frontiers = storage_frontiers.into_iter().map(|f| f.write_frontier);
1715
1716 let mut frontiers = compute_frontiers.into_iter().chain(storage_frontiers);
1717 let frontiers_ready =
1718 frontiers.all(|frontier| PartialOrder::less_than(&as_of, &frontier));
1719
1720 deps_scheduled && frontiers_ready
1721 };
1722
1723 if ready {
1724 self.send(ComputeCommand::Schedule(id));
1725 let collection = self.expect_collection_mut(id);
1726 collection.scheduled = true;
1727 }
1728 }
1729
1730 fn schedule_collections(&mut self) {
1732 let ids: Vec<_> = self.collections.keys().copied().collect();
1733 for id in ids {
1734 self.maybe_schedule_collection(id);
1735 }
1736 }
1737
1738 #[mz_ore::instrument(level = "debug")]
1741 pub fn drop_collections(&mut self, ids: Vec<GlobalId>) -> Result<(), CollectionMissing> {
1742 for id in &ids {
1743 let collection = self.collection_mut(*id)?;
1744
1745 collection.dropped = true;
1747
1748 collection.implied_read_hold.release();
1751 collection.warmup_read_hold.release();
1752
1753 self.subscribes.remove(id);
1756 self.copy_tos.remove(id);
1759 }
1760
1761 Ok(())
1762 }
1763
1764 #[mz_ore::instrument(level = "debug")]
1768 pub fn peek(
1769 &mut self,
1770 peek_target: PeekTarget,
1771 literal_constraints: Option<Vec<Row>>,
1772 uuid: Uuid,
1773 timestamp: Timestamp,
1774 result_desc: RelationDesc,
1775 finishing: RowSetFinishing,
1776 map_filter_project: mz_expr::SafeMfpPlan,
1777 mut read_hold: ReadHold,
1778 target_replica: Option<ReplicaId>,
1779 peek_response_tx: oneshot::Sender<PeekResponse>,
1780 ) -> Result<(), PeekError> {
1781 use PeekError::*;
1782
1783 let target_id = peek_target.id();
1784
1785 if read_hold.id() != target_id {
1787 return Err(ReadHoldIdMismatch(read_hold.id()));
1788 }
1789 read_hold
1790 .try_downgrade(Antichain::from_elem(timestamp.clone()))
1791 .map_err(|_| ReadHoldInsufficient(target_id))?;
1792
1793 if let Some(target) = target_replica {
1794 if !self.replica_exists(target) {
1795 return Err(ReplicaMissing(target));
1796 }
1797 }
1798
1799 let otel_ctx = OpenTelemetryContext::obtain();
1800
1801 self.peeks.insert(
1802 uuid,
1803 PendingPeek {
1804 target_replica,
1805 otel_ctx: otel_ctx.clone(),
1807 requested_at: Instant::now(),
1808 read_hold,
1809 peek_response_tx,
1810 limit: finishing.limit.map(usize::cast_from),
1811 offset: finishing.offset,
1812 },
1813 );
1814
1815 let peek = Peek {
1816 literal_constraints,
1817 uuid,
1818 timestamp,
1819 finishing,
1820 map_filter_project,
1821 otel_ctx,
1824 target: peek_target,
1825 result_desc,
1826 };
1827 self.send(ComputeCommand::Peek(Box::new(peek)));
1828
1829 Ok(())
1830 }
1831
1832 #[mz_ore::instrument(level = "debug")]
1834 pub fn cancel_peek(&mut self, uuid: Uuid, reason: PeekResponse) {
1835 let Some(peek) = self.peeks.get_mut(&uuid) else {
1836 tracing::warn!("did not find pending peek for {uuid}");
1837 return;
1838 };
1839
1840 let duration = peek.requested_at.elapsed();
1841 self.metrics
1842 .observe_peek_response(&PeekResponse::Canceled, duration);
1843
1844 let otel_ctx = peek.otel_ctx.clone();
1846 otel_ctx.attach_as_parent();
1847
1848 self.deliver_response(ComputeControllerResponse::PeekNotification(
1849 uuid,
1850 PeekNotification::Canceled,
1851 otel_ctx,
1852 ));
1853
1854 self.finish_peek(uuid, reason);
1857 }
1858
1859 #[mz_ore::instrument(level = "debug")]
1871 pub fn set_read_policy(
1872 &mut self,
1873 policies: Vec<(GlobalId, ReadPolicy)>,
1874 ) -> Result<(), ReadPolicyError> {
1875 for (id, _policy) in &policies {
1878 let collection = self.collection(*id)?;
1879 if collection.read_policy.is_none() {
1880 return Err(ReadPolicyError::WriteOnlyCollection(*id));
1881 }
1882 }
1883
1884 for (id, new_policy) in policies {
1885 let collection = self.expect_collection_mut(id);
1886 let new_since = new_policy.frontier(collection.write_frontier().borrow());
1887 let _ = collection.implied_read_hold.try_downgrade(new_since);
1888 collection.read_policy = Some(new_policy);
1889 }
1890
1891 Ok(())
1892 }
1893
1894 #[mz_ore::instrument(level = "debug")]
1902 fn maybe_update_global_write_frontier(
1903 &mut self,
1904 id: GlobalId,
1905 new_frontier: Antichain<Timestamp>,
1906 ) {
1907 let collection = self.expect_collection_mut(id);
1908
1909 let advanced = collection.shared.lock_write_frontier(|f| {
1910 let advanced = PartialOrder::less_than(f, &new_frontier);
1911 if advanced {
1912 f.clone_from(&new_frontier);
1913 }
1914 advanced
1915 });
1916
1917 if !advanced {
1918 return;
1919 }
1920
1921 let new_since = match &collection.read_policy {
1923 Some(read_policy) => {
1924 read_policy.frontier(new_frontier.borrow())
1927 }
1928 None => {
1929 Antichain::from_iter(
1938 new_frontier
1939 .iter()
1940 .map(|t| t.step_back().unwrap_or(Timestamp::MIN)),
1941 )
1942 }
1943 };
1944 let _ = collection.implied_read_hold.try_downgrade(new_since);
1945
1946 self.deliver_response(ComputeControllerResponse::FrontierUpper {
1948 id,
1949 upper: new_frontier,
1950 });
1951 }
1952
1953 pub(super) fn apply_read_hold_change(
1955 &mut self,
1956 id: GlobalId,
1957 mut update: ChangeBatch<Timestamp>,
1958 ) {
1959 let Some(collection) = self.collections.get_mut(&id) else {
1960 soft_panic_or_log!(
1961 "read hold change for absent collection (id={id}, changes={update:?})"
1962 );
1963 return;
1964 };
1965
1966 let new_since = collection.shared.lock_read_capabilities(|caps| {
1967 let read_frontier = caps.frontier();
1970 for (time, diff) in update.iter() {
1971 let count = caps.count_for(time) + diff;
1972 assert!(
1973 count >= 0,
1974 "invalid read capabilities update: negative capability \
1975 (id={id:?}, read_capabilities={caps:?}, update={update:?})",
1976 );
1977 assert!(
1978 count == 0 || read_frontier.less_equal(time),
1979 "invalid read capabilities update: frontier regression \
1980 (id={id:?}, read_capabilities={caps:?}, update={update:?})",
1981 );
1982 }
1983
1984 let changes = caps.update_iter(update.drain());
1987
1988 let changed = changes.count() > 0;
1989 changed.then(|| caps.frontier().to_owned())
1990 });
1991
1992 let Some(new_since) = new_since else {
1993 return; };
1995
1996 for read_hold in collection.compute_dependencies.values_mut() {
1998 read_hold
1999 .try_downgrade(new_since.clone())
2000 .expect("frontiers don't regress");
2001 }
2002 for read_hold in collection.storage_dependencies.values_mut() {
2003 read_hold
2004 .try_downgrade(new_since.clone())
2005 .expect("frontiers don't regress");
2006 }
2007
2008 self.send(ComputeCommand::AllowCompaction {
2010 id,
2011 frontier: new_since,
2012 });
2013 }
2014
2015 fn finish_peek(&mut self, uuid: Uuid, response: PeekResponse) {
2024 let Some(peek) = self.peeks.remove(&uuid) else {
2025 return;
2026 };
2027
2028 let _ = peek.peek_response_tx.send(response);
2030
2031 self.send(ComputeCommand::CancelPeek { uuid });
2034
2035 drop(peek.read_hold);
2036 }
2037
2038 fn handle_response(&mut self, (replica_id, epoch, response): ReplicaResponse) {
2041 if self
2043 .replicas
2044 .get(&replica_id)
2045 .filter(|replica| replica.epoch == epoch)
2046 .is_none()
2047 {
2048 return;
2049 }
2050
2051 match response {
2054 ComputeResponse::Frontiers(id, frontiers) => {
2055 self.handle_frontiers_response(id, frontiers, replica_id);
2056 }
2057 ComputeResponse::PeekResponse(uuid, peek_response, otel_ctx) => {
2058 self.handle_peek_response(uuid, peek_response, otel_ctx, replica_id);
2059 }
2060 ComputeResponse::CopyToResponse(id, response) => {
2061 self.handle_copy_to_response(id, response, replica_id);
2062 }
2063 ComputeResponse::SubscribeResponse(id, response) => {
2064 self.handle_subscribe_response(id, response, replica_id);
2065 }
2066 ComputeResponse::Status(response) => {
2067 self.handle_status_response(response, replica_id);
2068 }
2069 }
2070 }
2071
2072 fn handle_frontiers_response(
2075 &mut self,
2076 id: GlobalId,
2077 frontiers: FrontiersResponse,
2078 replica_id: ReplicaId,
2079 ) {
2080 if !self.collections.contains_key(&id) {
2081 soft_panic_or_log!(
2082 "frontiers update for an unknown collection \
2083 (id={id}, replica_id={replica_id}, frontiers={frontiers:?})"
2084 );
2085 return;
2086 }
2087 let Some(replica) = self.replicas.get_mut(&replica_id) else {
2088 soft_panic_or_log!(
2089 "frontiers update for an unknown replica \
2090 (replica_id={replica_id}, frontiers={frontiers:?})"
2091 );
2092 return;
2093 };
2094 let Some(replica_collection) = replica.collections.get_mut(&id) else {
2095 soft_panic_or_log!(
2096 "frontiers update for an unknown replica collection \
2097 (id={id}, replica_id={replica_id}, frontiers={frontiers:?})"
2098 );
2099 return;
2100 };
2101
2102 if let Some(new_frontier) = frontiers.input_frontier {
2103 replica_collection.update_input_frontier(new_frontier.clone());
2104 }
2105 if let Some(new_frontier) = frontiers.output_frontier {
2106 replica_collection.update_output_frontier(new_frontier.clone());
2107 }
2108 if let Some(new_frontier) = frontiers.write_frontier {
2109 replica_collection.update_write_frontier(new_frontier.clone());
2110 self.maybe_update_global_write_frontier(id, new_frontier);
2111 }
2112 }
2113
2114 #[mz_ore::instrument(level = "debug")]
2115 fn handle_peek_response(
2116 &mut self,
2117 uuid: Uuid,
2118 response: PeekResponse,
2119 otel_ctx: OpenTelemetryContext,
2120 replica_id: ReplicaId,
2121 ) {
2122 otel_ctx.attach_as_parent();
2123
2124 let Some(peek) = self.peeks.get(&uuid) else {
2127 return;
2128 };
2129
2130 let target_replica = peek.target_replica.unwrap_or(replica_id);
2132 if target_replica != replica_id {
2133 return;
2134 }
2135
2136 let duration = peek.requested_at.elapsed();
2137 self.metrics.observe_peek_response(&response, duration);
2138
2139 let notification = PeekNotification::new(&response, peek.offset, peek.limit);
2140 self.deliver_response(ComputeControllerResponse::PeekNotification(
2143 uuid,
2144 notification,
2145 otel_ctx,
2146 ));
2147
2148 self.finish_peek(uuid, response)
2149 }
2150
2151 fn handle_copy_to_response(
2152 &mut self,
2153 sink_id: GlobalId,
2154 response: CopyToResponse,
2155 replica_id: ReplicaId,
2156 ) {
2157 if !self.collections.contains_key(&sink_id) {
2158 soft_panic_or_log!(
2159 "received response for an unknown copy-to \
2160 (sink_id={sink_id}, replica_id={replica_id})",
2161 );
2162 return;
2163 }
2164 let Some(replica) = self.replicas.get_mut(&replica_id) else {
2165 soft_panic_or_log!("copy-to response for an unknown replica (replica_id={replica_id})");
2166 return;
2167 };
2168 let Some(replica_collection) = replica.collections.get_mut(&sink_id) else {
2169 soft_panic_or_log!(
2170 "copy-to response for an unknown replica collection \
2171 (sink_id={sink_id}, replica_id={replica_id})"
2172 );
2173 return;
2174 };
2175
2176 replica_collection.update_write_frontier(Antichain::new());
2180 replica_collection.update_input_frontier(Antichain::new());
2181 replica_collection.update_output_frontier(Antichain::new());
2182
2183 if !self.copy_tos.remove(&sink_id) {
2186 return;
2187 }
2188
2189 let result = match response {
2190 CopyToResponse::RowCount(count) => Ok(count),
2191 CopyToResponse::Error(error) => Err(anyhow::anyhow!(error)),
2192 CopyToResponse::Dropped => {
2197 tracing::error!(
2198 %sink_id, %replica_id,
2199 "received `Dropped` response for a tracked copy to",
2200 );
2201 return;
2202 }
2203 };
2204
2205 self.deliver_response(ComputeControllerResponse::CopyToResponse(sink_id, result));
2206 }
2207
2208 fn handle_subscribe_response(
2209 &mut self,
2210 subscribe_id: GlobalId,
2211 response: SubscribeResponse,
2212 replica_id: ReplicaId,
2213 ) {
2214 if !self.collections.contains_key(&subscribe_id) {
2215 soft_panic_or_log!(
2216 "received response for an unknown subscribe \
2217 (subscribe_id={subscribe_id}, replica_id={replica_id})",
2218 );
2219 return;
2220 }
2221 let Some(replica) = self.replicas.get_mut(&replica_id) else {
2222 soft_panic_or_log!(
2223 "subscribe response for an unknown replica (replica_id={replica_id})"
2224 );
2225 return;
2226 };
2227 let Some(replica_collection) = replica.collections.get_mut(&subscribe_id) else {
2228 soft_panic_or_log!(
2229 "subscribe response for an unknown replica collection \
2230 (subscribe_id={subscribe_id}, replica_id={replica_id})"
2231 );
2232 return;
2233 };
2234
2235 let write_frontier = match &response {
2239 SubscribeResponse::Batch(batch) => batch.upper.clone(),
2240 SubscribeResponse::DroppedAt(_) => Antichain::new(),
2241 };
2242
2243 replica_collection.update_write_frontier(write_frontier.clone());
2247 replica_collection.update_input_frontier(write_frontier.clone());
2248 replica_collection.update_output_frontier(write_frontier.clone());
2249
2250 let Some(mut subscribe) = self.subscribes.get(&subscribe_id).cloned() else {
2252 return;
2253 };
2254
2255 self.maybe_update_global_write_frontier(subscribe_id, write_frontier);
2261
2262 match response {
2263 SubscribeResponse::Batch(batch) => {
2264 let upper = batch.upper;
2265 let mut updates = batch.updates;
2266
2267 if PartialOrder::less_than(&subscribe.frontier, &upper) {
2270 let lower = std::mem::replace(&mut subscribe.frontier, upper.clone());
2271
2272 if upper.is_empty() {
2273 self.subscribes.remove(&subscribe_id);
2275 } else {
2276 self.subscribes.insert(subscribe_id, subscribe);
2278 }
2279
2280 if let Ok(updates) = updates.as_mut() {
2281 updates.retain_mut(|updates| {
2282 let offset = updates.times().partition_point(|t| {
2283 !lower.less_equal(t)
2286 });
2287 let (_, past_lower) = std::mem::take(updates).split_at(offset);
2288 *updates = past_lower;
2289 updates.len() > 0
2290 });
2291 }
2292 self.deliver_response(ComputeControllerResponse::SubscribeResponse(
2293 subscribe_id,
2294 SubscribeBatch {
2295 lower,
2296 upper,
2297 updates,
2298 },
2299 ));
2300 }
2301 }
2302 SubscribeResponse::DroppedAt(frontier) => {
2303 tracing::error!(
2308 %subscribe_id,
2309 %replica_id,
2310 frontier = ?frontier.elements(),
2311 "received `DroppedAt` response for a tracked subscribe",
2312 );
2313 self.subscribes.remove(&subscribe_id);
2314 }
2315 }
2316 }
2317
2318 fn handle_status_response(&self, response: StatusResponse, _replica_id: ReplicaId) {
2319 match response {
2320 StatusResponse::Placeholder => {}
2321 }
2322 }
2323
2324 fn dependency_write_frontiers<'b>(
2326 &'b self,
2327 collection: &'b CollectionState,
2328 ) -> impl Iterator<Item = Antichain<Timestamp>> + 'b {
2329 let compute_frontiers = collection.compute_dependency_ids().filter_map(|dep_id| {
2330 let collection = self.collections.get(&dep_id);
2331 collection.map(|c| c.write_frontier())
2332 });
2333 let storage_frontiers = collection.storage_dependency_ids().filter_map(|dep_id| {
2334 let frontiers = self.storage_collections.collection_frontiers(dep_id).ok();
2335 frontiers.map(|f| f.write_frontier)
2336 });
2337
2338 compute_frontiers.chain(storage_frontiers)
2339 }
2340
2341 fn transitive_storage_dependency_write_frontiers<'b>(
2343 &'b self,
2344 collection: &'b CollectionState,
2345 ) -> impl Iterator<Item = Antichain<Timestamp>> + 'b {
2346 let mut storage_ids: BTreeSet<_> = collection.storage_dependency_ids().collect();
2347 let mut todo: Vec<_> = collection.compute_dependency_ids().collect();
2348 let mut done = BTreeSet::new();
2349
2350 while let Some(id) = todo.pop() {
2351 if done.contains(&id) {
2352 continue;
2353 }
2354 if let Some(dep) = self.collections.get(&id) {
2355 storage_ids.extend(dep.storage_dependency_ids());
2356 todo.extend(dep.compute_dependency_ids())
2357 }
2358 done.insert(id);
2359 }
2360
2361 let storage_frontiers = storage_ids.into_iter().filter_map(|id| {
2362 let frontiers = self.storage_collections.collection_frontiers(id).ok();
2363 frontiers.map(|f| f.write_frontier)
2364 });
2365
2366 storage_frontiers
2367 }
2368
2369 fn downgrade_warmup_capabilities(&mut self) {
2382 let mut new_capabilities = BTreeMap::new();
2383 for (id, collection) in &self.collections {
2384 if collection.read_policy.is_none()
2388 && collection.shared.lock_write_frontier(|f| f.is_empty())
2389 {
2390 new_capabilities.insert(*id, Antichain::new());
2391 continue;
2392 }
2393
2394 let mut new_capability = Antichain::new();
2395 for frontier in self.dependency_write_frontiers(collection) {
2396 for time in frontier {
2397 new_capability.insert(time.step_back().unwrap_or(time));
2398 }
2399 }
2400
2401 new_capabilities.insert(*id, new_capability);
2402 }
2403
2404 for (id, new_capability) in new_capabilities {
2405 let collection = self.expect_collection_mut(id);
2406 let _ = collection.warmup_read_hold.try_downgrade(new_capability);
2407 }
2408 }
2409
2410 fn forward_implied_capabilities(&mut self) {
2438 if !ENABLE_PAUSED_CLUSTER_READHOLD_DOWNGRADE.get(&self.dyncfg) {
2439 return;
2440 }
2441 if !self.replicas.is_empty() {
2442 return;
2443 }
2444
2445 let mut new_capabilities = BTreeMap::new();
2446 for (id, collection) in &self.collections {
2447 let Some(read_policy) = &collection.read_policy else {
2448 continue;
2450 };
2451
2452 let mut dep_frontier = Antichain::new();
2456 for frontier in self.transitive_storage_dependency_write_frontiers(collection) {
2457 dep_frontier.extend(frontier);
2458 }
2459
2460 let new_capability = read_policy.frontier(dep_frontier.borrow());
2461 if PartialOrder::less_than(collection.implied_read_hold.since(), &new_capability) {
2462 new_capabilities.insert(*id, new_capability);
2463 }
2464 }
2465
2466 for (id, new_capability) in new_capabilities {
2467 let collection = self.expect_collection_mut(id);
2468 let _ = collection.implied_read_hold.try_downgrade(new_capability);
2469 }
2470 }
2471
2472 pub(super) fn acquire_read_hold(&self, id: GlobalId) -> Result<ReadHold, CollectionMissing> {
2477 let collection = self.collection(id)?;
2483 let since = collection.shared.lock_read_capabilities(|caps| {
2484 let since = caps.frontier().to_owned();
2485 caps.update_iter(since.iter().map(|t| (t.clone(), 1)));
2486 since
2487 });
2488 let hold = ReadHold::new(id, since, Arc::clone(&self.read_hold_tx));
2489 Ok(hold)
2490 }
2491
2492 #[mz_ore::instrument(level = "debug")]
2498 pub fn maintain(&mut self) {
2499 self.rehydrate_failed_replicas();
2500 self.downgrade_warmup_capabilities();
2501 self.forward_implied_capabilities();
2502 self.schedule_collections();
2503 self.cleanup_collections();
2504 self.update_frontier_introspection();
2505 self.refresh_state_metrics();
2506 self.refresh_wallclock_lag();
2507 }
2508}
2509
2510#[derive(Debug)]
2515struct CollectionState {
2516 target_replica: Option<ReplicaId>,
2518 log_collection: bool,
2522 dropped: bool,
2528 scheduled: bool,
2531
2532 read_only: bool,
2536
2537 shared: SharedCollectionState,
2539
2540 implied_read_hold: ReadHold,
2547 warmup_read_hold: ReadHold,
2555 read_policy: Option<ReadPolicy>,
2561
2562 storage_dependencies: BTreeMap<GlobalId, ReadHold>,
2565 compute_dependencies: BTreeMap<GlobalId, ReadHold>,
2568
2569 introspection: CollectionIntrospection,
2571
2572 wallclock_lag_histogram_stash: Option<
2579 BTreeMap<
2580 (
2581 WallclockLagHistogramPeriod,
2582 WallclockLag,
2583 BTreeMap<&'static str, String>,
2584 ),
2585 Diff,
2586 >,
2587 >,
2588}
2589
2590impl CollectionState {
2591 fn new(
2593 collection_id: GlobalId,
2594 as_of: Antichain<Timestamp>,
2595 shared: SharedCollectionState,
2596 storage_dependencies: BTreeMap<GlobalId, ReadHold>,
2597 compute_dependencies: BTreeMap<GlobalId, ReadHold>,
2598 read_hold_tx: read_holds::ChangeTx,
2599 introspection: CollectionIntrospection,
2600 ) -> Self {
2601 let since = as_of.clone();
2603 let upper = as_of;
2605
2606 assert!(shared.lock_read_capabilities(|c| c.frontier() == since.borrow()));
2608 assert!(shared.lock_write_frontier(|f| f == &upper));
2609
2610 let implied_read_hold =
2614 ReadHold::new(collection_id, since.clone(), Arc::clone(&read_hold_tx));
2615 let warmup_read_hold = ReadHold::new(collection_id, since.clone(), read_hold_tx);
2616
2617 let updates = warmup_read_hold.since().iter().map(|t| (t.clone(), 1));
2618 shared.lock_read_capabilities(|c| {
2619 c.update_iter(updates);
2620 });
2621
2622 let wallclock_lag_histogram_stash = match collection_id.is_transient() {
2626 true => None,
2627 false => Some(Default::default()),
2628 };
2629
2630 Self {
2631 target_replica: None,
2632 log_collection: false,
2633 dropped: false,
2634 scheduled: false,
2635 read_only: true,
2636 shared,
2637 implied_read_hold,
2638 warmup_read_hold,
2639 read_policy: Some(ReadPolicy::ValidFrom(since)),
2640 storage_dependencies,
2641 compute_dependencies,
2642 introspection,
2643 wallclock_lag_histogram_stash,
2644 }
2645 }
2646
2647 fn new_log_collection(
2649 id: GlobalId,
2650 shared: SharedCollectionState,
2651 read_hold_tx: read_holds::ChangeTx,
2652 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
2653 ) -> Self {
2654 let since = Antichain::from_elem(Timestamp::MIN);
2655 let introspection = CollectionIntrospection::new(
2656 id,
2657 introspection_tx,
2658 since.clone(),
2659 false,
2660 None,
2661 None,
2662 Vec::new(),
2663 );
2664 let mut state = Self::new(
2665 id,
2666 since,
2667 shared,
2668 Default::default(),
2669 Default::default(),
2670 read_hold_tx,
2671 introspection,
2672 );
2673 state.log_collection = true;
2674 state.scheduled = true;
2676 state
2677 }
2678
2679 fn read_frontier(&self) -> Antichain<Timestamp> {
2681 self.shared
2682 .lock_read_capabilities(|c| c.frontier().to_owned())
2683 }
2684
2685 fn write_frontier(&self) -> Antichain<Timestamp> {
2687 self.shared.lock_write_frontier(|f| f.clone())
2688 }
2689
2690 fn storage_dependency_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
2691 self.storage_dependencies.keys().copied()
2692 }
2693
2694 fn compute_dependency_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
2695 self.compute_dependencies.keys().copied()
2696 }
2697}
2698
2699#[derive(Clone, Debug)]
2710pub(super) struct SharedCollectionState {
2711 read_capabilities: Arc<Mutex<MutableAntichain<Timestamp>>>,
2724 write_frontier: Arc<Mutex<Antichain<Timestamp>>>,
2726}
2727
2728impl SharedCollectionState {
2729 pub fn new(as_of: Antichain<Timestamp>) -> Self {
2730 let since = as_of.clone();
2732 let upper = as_of;
2734
2735 let mut read_capabilities = MutableAntichain::new();
2739 read_capabilities.update_iter(since.iter().map(|time| (time.clone(), 1)));
2740
2741 Self {
2742 read_capabilities: Arc::new(Mutex::new(read_capabilities)),
2743 write_frontier: Arc::new(Mutex::new(upper)),
2744 }
2745 }
2746
2747 pub fn lock_read_capabilities<F, R>(&self, f: F) -> R
2748 where
2749 F: FnOnce(&mut MutableAntichain<Timestamp>) -> R,
2750 {
2751 let mut caps = self.read_capabilities.lock().expect("poisoned");
2752 f(&mut *caps)
2753 }
2754
2755 pub fn lock_write_frontier<F, R>(&self, f: F) -> R
2756 where
2757 F: FnOnce(&mut Antichain<Timestamp>) -> R,
2758 {
2759 let mut frontier = self.write_frontier.lock().expect("poisoned");
2760 f(&mut *frontier)
2761 }
2762}
2763
2764#[derive(Debug)]
2767struct CollectionIntrospection {
2768 collection_id: GlobalId,
2770 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
2772 frontiers: Option<FrontiersIntrospectionState>,
2777 refresh: Option<RefreshIntrospectionState>,
2781 dependency_ids: Vec<GlobalId>,
2783}
2784
2785impl CollectionIntrospection {
2786 fn new(
2787 collection_id: GlobalId,
2788 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
2789 as_of: Antichain<Timestamp>,
2790 storage_sink: bool,
2791 initial_as_of: Option<Antichain<Timestamp>>,
2792 refresh_schedule: Option<RefreshSchedule>,
2793 dependency_ids: Vec<GlobalId>,
2794 ) -> Self {
2795 let refresh =
2796 match (refresh_schedule, initial_as_of) {
2797 (Some(refresh_schedule), Some(initial_as_of)) => Some(
2798 RefreshIntrospectionState::new(refresh_schedule, initial_as_of, &as_of),
2799 ),
2800 (refresh_schedule, _) => {
2801 soft_assert_or_log!(
2804 refresh_schedule.is_none(),
2805 "`refresh_schedule` without an `initial_as_of`: {collection_id}"
2806 );
2807 None
2808 }
2809 };
2810 let frontiers = (!storage_sink).then(|| FrontiersIntrospectionState::new(as_of));
2811
2812 let self_ = Self {
2813 collection_id,
2814 introspection_tx,
2815 frontiers,
2816 refresh,
2817 dependency_ids,
2818 };
2819
2820 self_.report_initial_state();
2821 self_
2822 }
2823
2824 fn report_initial_state(&self) {
2826 if let Some(frontiers) = &self.frontiers {
2827 let row = frontiers.row_for_collection(self.collection_id);
2828 let updates = vec![(row, Diff::ONE)];
2829 self.send(IntrospectionType::Frontiers, updates);
2830 }
2831
2832 if let Some(refresh) = &self.refresh {
2833 let row = refresh.row_for_collection(self.collection_id);
2834 let updates = vec![(row, Diff::ONE)];
2835 self.send(IntrospectionType::ComputeMaterializedViewRefreshes, updates);
2836 }
2837
2838 if !self.dependency_ids.is_empty() {
2839 let updates = self.dependency_rows(Diff::ONE);
2840 self.send(IntrospectionType::ComputeDependencies, updates);
2841 }
2842 }
2843
2844 fn dependency_rows(&self, diff: Diff) -> Vec<(Row, Diff)> {
2846 self.dependency_ids
2847 .iter()
2848 .map(|dependency_id| {
2849 let row = Row::pack_slice(&[
2850 Datum::String(&self.collection_id.to_string()),
2851 Datum::String(&dependency_id.to_string()),
2852 ]);
2853 (row, diff)
2854 })
2855 .collect()
2856 }
2857
2858 fn observe_frontiers(
2861 &mut self,
2862 read_frontier: &Antichain<Timestamp>,
2863 write_frontier: &Antichain<Timestamp>,
2864 ) {
2865 self.update_frontier_introspection(read_frontier, write_frontier);
2866 self.update_refresh_introspection(write_frontier);
2867 }
2868
2869 fn update_frontier_introspection(
2870 &mut self,
2871 read_frontier: &Antichain<Timestamp>,
2872 write_frontier: &Antichain<Timestamp>,
2873 ) {
2874 let Some(frontiers) = &mut self.frontiers else {
2875 return;
2876 };
2877
2878 if &frontiers.read_frontier == read_frontier && &frontiers.write_frontier == write_frontier
2879 {
2880 return; };
2882
2883 let retraction = frontiers.row_for_collection(self.collection_id);
2884 frontiers.update(read_frontier, write_frontier);
2885 let insertion = frontiers.row_for_collection(self.collection_id);
2886 let updates = vec![(retraction, Diff::MINUS_ONE), (insertion, Diff::ONE)];
2887 self.send(IntrospectionType::Frontiers, updates);
2888 }
2889
2890 fn update_refresh_introspection(&mut self, write_frontier: &Antichain<Timestamp>) {
2891 let Some(refresh) = &mut self.refresh else {
2892 return;
2893 };
2894
2895 let retraction = refresh.row_for_collection(self.collection_id);
2896 refresh.frontier_update(write_frontier);
2897 let insertion = refresh.row_for_collection(self.collection_id);
2898
2899 if retraction == insertion {
2900 return; }
2902
2903 let updates = vec![(retraction, Diff::MINUS_ONE), (insertion, Diff::ONE)];
2904 self.send(IntrospectionType::ComputeMaterializedViewRefreshes, updates);
2905 }
2906
2907 fn send(&self, introspection_type: IntrospectionType, updates: Vec<(Row, Diff)>) {
2908 let _ = self.introspection_tx.send((introspection_type, updates));
2911 }
2912}
2913
2914impl Drop for CollectionIntrospection {
2915 fn drop(&mut self) {
2916 if let Some(frontiers) = &self.frontiers {
2918 let row = frontiers.row_for_collection(self.collection_id);
2919 let updates = vec![(row, Diff::MINUS_ONE)];
2920 self.send(IntrospectionType::Frontiers, updates);
2921 }
2922
2923 if let Some(refresh) = &self.refresh {
2925 let retraction = refresh.row_for_collection(self.collection_id);
2926 let updates = vec![(retraction, Diff::MINUS_ONE)];
2927 self.send(IntrospectionType::ComputeMaterializedViewRefreshes, updates);
2928 }
2929
2930 if !self.dependency_ids.is_empty() {
2932 let updates = self.dependency_rows(Diff::MINUS_ONE);
2933 self.send(IntrospectionType::ComputeDependencies, updates);
2934 }
2935 }
2936}
2937
2938#[derive(Debug)]
2939struct FrontiersIntrospectionState {
2940 read_frontier: Antichain<Timestamp>,
2941 write_frontier: Antichain<Timestamp>,
2942}
2943
2944impl FrontiersIntrospectionState {
2945 fn new(as_of: Antichain<Timestamp>) -> Self {
2946 Self {
2947 read_frontier: as_of.clone(),
2948 write_frontier: as_of,
2949 }
2950 }
2951
2952 fn row_for_collection(&self, collection_id: GlobalId) -> Row {
2954 let read_frontier = self
2955 .read_frontier
2956 .as_option()
2957 .map_or(Datum::Null, |ts| ts.clone().into());
2958 let write_frontier = self
2959 .write_frontier
2960 .as_option()
2961 .map_or(Datum::Null, |ts| ts.clone().into());
2962 Row::pack_slice(&[
2963 Datum::String(&collection_id.to_string()),
2964 read_frontier,
2965 write_frontier,
2966 ])
2967 }
2968
2969 fn update(
2971 &mut self,
2972 read_frontier: &Antichain<Timestamp>,
2973 write_frontier: &Antichain<Timestamp>,
2974 ) {
2975 if read_frontier != &self.read_frontier {
2976 self.read_frontier.clone_from(read_frontier);
2977 }
2978 if write_frontier != &self.write_frontier {
2979 self.write_frontier.clone_from(write_frontier);
2980 }
2981 }
2982}
2983
2984#[derive(Debug)]
2987struct RefreshIntrospectionState {
2988 refresh_schedule: RefreshSchedule,
2990 initial_as_of: Antichain<Timestamp>,
2991 next_refresh: Datum<'static>, last_completed_refresh: Datum<'static>, }
2995
2996impl RefreshIntrospectionState {
2997 fn row_for_collection(&self, collection_id: GlobalId) -> Row {
2999 Row::pack_slice(&[
3000 Datum::String(&collection_id.to_string()),
3001 self.last_completed_refresh,
3002 self.next_refresh,
3003 ])
3004 }
3005}
3006
3007impl RefreshIntrospectionState {
3008 fn new(
3011 refresh_schedule: RefreshSchedule,
3012 initial_as_of: Antichain<Timestamp>,
3013 upper: &Antichain<Timestamp>,
3014 ) -> Self {
3015 let mut self_ = Self {
3016 refresh_schedule: refresh_schedule.clone(),
3017 initial_as_of: initial_as_of.clone(),
3018 next_refresh: Datum::Null,
3019 last_completed_refresh: Datum::Null,
3020 };
3021 self_.frontier_update(upper);
3022 self_
3023 }
3024
3025 fn frontier_update(&mut self, write_frontier: &Antichain<Timestamp>) {
3028 if write_frontier.is_empty() {
3029 self.last_completed_refresh =
3030 if let Some(last_refresh) = self.refresh_schedule.last_refresh() {
3031 last_refresh.into()
3032 } else {
3033 Timestamp::MAX.into()
3036 };
3037 self.next_refresh = Datum::Null;
3038 } else {
3039 if PartialOrder::less_equal(write_frontier, &self.initial_as_of) {
3040 self.last_completed_refresh = Datum::Null;
3042 let initial_as_of = self.initial_as_of.as_option().expect(
3043 "initial_as_of can't be [], because then there would be no refreshes at all",
3044 );
3045 let first_refresh = self
3046 .refresh_schedule
3047 .round_up_timestamp(*initial_as_of)
3048 .expect("sequencing makes sure that REFRESH MVs always have a first refresh");
3049 soft_assert_or_log!(
3050 first_refresh == *initial_as_of,
3051 "initial_as_of should be set to the first refresh"
3052 );
3053 self.next_refresh = first_refresh.into();
3054 } else {
3055 let write_frontier = write_frontier.as_option().expect("checked above");
3057 self.last_completed_refresh = self
3058 .refresh_schedule
3059 .round_down_timestamp_m1(*write_frontier)
3060 .map_or_else(
3061 || {
3062 soft_panic_or_log!(
3063 "rounding down should have returned the first refresh or later"
3064 );
3065 Datum::Null
3066 },
3067 |last_completed_refresh| last_completed_refresh.into(),
3068 );
3069 self.next_refresh = write_frontier.clone().into();
3070 }
3071 }
3072 }
3073}
3074
3075#[derive(Debug)]
3077struct PendingPeek {
3078 target_replica: Option<ReplicaId>,
3082 otel_ctx: OpenTelemetryContext,
3084 requested_at: Instant,
3088 read_hold: ReadHold,
3090 peek_response_tx: oneshot::Sender<PeekResponse>,
3092 limit: Option<usize>,
3094 offset: usize,
3096}
3097
3098#[derive(Debug, Clone)]
3099struct ActiveSubscribe {
3100 frontier: Antichain<Timestamp>,
3102}
3103
3104impl Default for ActiveSubscribe {
3105 fn default() -> Self {
3106 Self {
3107 frontier: Antichain::from_elem(Timestamp::MIN),
3108 }
3109 }
3110}
3111
3112#[derive(Debug)]
3114struct ReplicaState {
3115 id: ReplicaId,
3117 client: ReplicaClient,
3119 config: ReplicaConfig,
3121 metrics: ReplicaMetrics,
3123 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3125 collections: BTreeMap<GlobalId, ReplicaCollectionState>,
3127 epoch: u64,
3129 dyncfg_override: ConfigUpdates,
3133}
3134
3135impl ReplicaState {
3136 fn new(
3137 id: ReplicaId,
3138 client: ReplicaClient,
3139 config: ReplicaConfig,
3140 metrics: ReplicaMetrics,
3141 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3142 epoch: u64,
3143 dyncfg_override: ConfigUpdates,
3144 ) -> Self {
3145 Self {
3146 id,
3147 client,
3148 config,
3149 metrics,
3150 introspection_tx,
3151 epoch,
3152 dyncfg_override,
3153 collections: Default::default(),
3154 }
3155 }
3156
3157 fn add_collection(
3163 &mut self,
3164 id: GlobalId,
3165 as_of: Antichain<Timestamp>,
3166 input_read_holds: Vec<ReadHold>,
3167 ) {
3168 let metrics = self.metrics.for_collection(id);
3169 let introspection = ReplicaCollectionIntrospection::new(
3170 self.id,
3171 id,
3172 self.introspection_tx.clone(),
3173 as_of.clone(),
3174 );
3175 let mut state =
3176 ReplicaCollectionState::new(metrics, as_of, introspection, input_read_holds);
3177
3178 if id.is_transient() {
3182 state.wallclock_lag_max = None;
3183 }
3184
3185 if let Some(previous) = self.collections.insert(id, state) {
3186 panic!("attempt to add a collection with existing ID {id} (previous={previous:?}");
3187 }
3188 }
3189
3190 fn remove_collection(&mut self, id: GlobalId) -> Option<ReplicaCollectionState> {
3192 self.collections.remove(&id)
3193 }
3194
3195 fn expect_collection(&self, id: GlobalId) -> &ReplicaCollectionState {
3202 self.collections
3203 .get(&id)
3204 .expect("hosting replica must have per-replica collection state")
3205 }
3206
3207 fn collection_frontiers_empty(&self, id: GlobalId) -> bool {
3209 self.collections.get(&id).map_or(true, |c| {
3210 c.write_frontier.is_empty()
3211 && c.input_frontier.is_empty()
3212 && c.output_frontier.is_empty()
3213 })
3214 }
3215
3216 #[mz_ore::instrument(level = "debug")]
3220 pub fn dump(&self) -> Result<serde_json::Value, anyhow::Error> {
3221 let Self {
3228 id,
3229 client: _,
3230 config: _,
3231 metrics: _,
3232 introspection_tx: _,
3233 epoch,
3234 dyncfg_override,
3235 collections,
3236 } = self;
3237
3238 let collections: BTreeMap<_, _> = collections
3239 .iter()
3240 .map(|(id, collection)| (id.to_string(), format!("{collection:?}")))
3241 .collect();
3242
3243 Ok(serde_json::json!({
3244 "id": id.to_string(),
3245 "collections": collections,
3246 "epoch": epoch,
3247 "dyncfg_override": format!("{dyncfg_override:?}"),
3248 }))
3249 }
3250}
3251
3252#[derive(Debug)]
3253struct ReplicaCollectionState {
3254 write_frontier: Antichain<Timestamp>,
3258 input_frontier: Antichain<Timestamp>,
3262 output_frontier: Antichain<Timestamp>,
3266
3267 metrics: Option<ReplicaCollectionMetrics>,
3271 as_of: Antichain<Timestamp>,
3273 introspection: ReplicaCollectionIntrospection,
3275 input_read_holds: Vec<ReadHold>,
3281
3282 wallclock_lag_max: Option<WallclockLag>,
3286}
3287
3288impl ReplicaCollectionState {
3289 fn new(
3290 metrics: Option<ReplicaCollectionMetrics>,
3291 as_of: Antichain<Timestamp>,
3292 introspection: ReplicaCollectionIntrospection,
3293 input_read_holds: Vec<ReadHold>,
3294 ) -> Self {
3295 Self {
3296 write_frontier: as_of.clone(),
3297 input_frontier: as_of.clone(),
3298 output_frontier: as_of.clone(),
3299 metrics,
3300 as_of,
3301 introspection,
3302 input_read_holds,
3303 wallclock_lag_max: Some(WallclockLag::MIN),
3304 }
3305 }
3306
3307 fn hydrated(&self) -> bool {
3309 self.as_of.is_empty() || PartialOrder::less_than(&self.as_of, &self.output_frontier)
3325 }
3326
3327 fn update_write_frontier(&mut self, new_frontier: Antichain<Timestamp>) {
3329 if PartialOrder::less_than(&new_frontier, &self.write_frontier) {
3330 soft_panic_or_log!(
3331 "replica collection write frontier regression (old={:?}, new={new_frontier:?})",
3332 self.write_frontier,
3333 );
3334 return;
3335 } else if new_frontier == self.write_frontier {
3336 return;
3337 }
3338
3339 self.write_frontier = new_frontier;
3340 }
3341
3342 fn update_input_frontier(&mut self, new_frontier: Antichain<Timestamp>) {
3344 if PartialOrder::less_than(&new_frontier, &self.input_frontier) {
3345 soft_panic_or_log!(
3346 "replica collection input frontier regression (old={:?}, new={new_frontier:?})",
3347 self.input_frontier,
3348 );
3349 return;
3350 } else if new_frontier == self.input_frontier {
3351 return;
3352 }
3353
3354 self.input_frontier = new_frontier;
3355
3356 for read_hold in &mut self.input_read_holds {
3358 let result = read_hold.try_downgrade(self.input_frontier.clone());
3359 soft_assert_or_log!(
3360 result.is_ok(),
3361 "read hold downgrade failed (read_hold={read_hold:?}, new_since={:?})",
3362 self.input_frontier,
3363 );
3364 }
3365 }
3366
3367 fn update_output_frontier(&mut self, new_frontier: Antichain<Timestamp>) {
3369 if PartialOrder::less_than(&new_frontier, &self.output_frontier) {
3370 soft_panic_or_log!(
3371 "replica collection output frontier regression (old={:?}, new={new_frontier:?})",
3372 self.output_frontier,
3373 );
3374 return;
3375 } else if new_frontier == self.output_frontier {
3376 return;
3377 }
3378
3379 self.output_frontier = new_frontier;
3380 }
3381}
3382
3383#[derive(Debug)]
3386struct ReplicaCollectionIntrospection {
3387 replica_id: ReplicaId,
3389 collection_id: GlobalId,
3391 write_frontier: Antichain<Timestamp>,
3393 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3395}
3396
3397impl ReplicaCollectionIntrospection {
3398 fn new(
3400 replica_id: ReplicaId,
3401 collection_id: GlobalId,
3402 introspection_tx: mpsc::UnboundedSender<IntrospectionUpdates>,
3403 as_of: Antichain<Timestamp>,
3404 ) -> Self {
3405 let self_ = Self {
3406 replica_id,
3407 collection_id,
3408 write_frontier: as_of,
3409 introspection_tx,
3410 };
3411
3412 self_.report_initial_state();
3413 self_
3414 }
3415
3416 fn report_initial_state(&self) {
3418 let row = self.write_frontier_row();
3419 let updates = vec![(row, Diff::ONE)];
3420 self.send(IntrospectionType::ReplicaFrontiers, updates);
3421 }
3422
3423 fn observe_frontier(&mut self, write_frontier: &Antichain<Timestamp>) {
3425 if self.write_frontier == *write_frontier {
3426 return; }
3428
3429 let retraction = self.write_frontier_row();
3430 self.write_frontier.clone_from(write_frontier);
3431 let insertion = self.write_frontier_row();
3432
3433 let updates = vec![(retraction, Diff::MINUS_ONE), (insertion, Diff::ONE)];
3434 self.send(IntrospectionType::ReplicaFrontiers, updates);
3435 }
3436
3437 fn write_frontier_row(&self) -> Row {
3439 let write_frontier = self
3440 .write_frontier
3441 .as_option()
3442 .map_or(Datum::Null, |ts| ts.clone().into());
3443 Row::pack_slice(&[
3444 Datum::String(&self.collection_id.to_string()),
3445 Datum::String(&self.replica_id.to_string()),
3446 write_frontier,
3447 ])
3448 }
3449
3450 fn send(&self, introspection_type: IntrospectionType, updates: Vec<(Row, Diff)>) {
3451 let _ = self.introspection_tx.send((introspection_type, updates));
3454 }
3455}
3456
3457impl Drop for ReplicaCollectionIntrospection {
3458 fn drop(&mut self) {
3459 let row = self.write_frontier_row();
3461 let updates = vec![(row, Diff::MINUS_ONE)];
3462 self.send(IntrospectionType::ReplicaFrontiers, updates);
3463 }
3464}
3465
3466fn classify_collection_readiness<'a, I>(
3472 replicas: I,
3473 target_replica_ids: &BTreeSet<ReplicaId>,
3474 reference_replica_ids: &BTreeSet<ReplicaId>,
3475 allowed_lag: Option<Timestamp>,
3476) -> CollectionReadiness
3477where
3478 I: Iterator<Item = (ReplicaId, &'a ReplicaCollectionState)> + Clone,
3479{
3480 let lag = allowed_lag.map(|allowed_lag| {
3481 let mut reference = Antichain::from_elem(Timestamp::MIN);
3482 for (_, state) in replicas
3483 .clone()
3484 .filter(|(id, _)| reference_replica_ids.contains(id))
3485 {
3486 reference.join_assign(&state.output_frontier);
3487 }
3488 (reference, allowed_lag)
3489 });
3490
3491 let mut result = CollectionReadiness::Unhydrated;
3492 for (_, state) in replicas.filter(|(id, _)| target_replica_ids.contains(id)) {
3493 match CollectionReadiness::classify(
3494 state.hydrated(),
3495 &state.output_frontier,
3496 lag.as_ref().map(|(reference, lag)| (reference, *lag)),
3497 ) {
3498 CollectionReadiness::Ready => return CollectionReadiness::Ready,
3499 CollectionReadiness::Lagging { lag } => {
3500 let lag = match result {
3503 CollectionReadiness::Lagging {
3504 lag: Some(previous),
3505 } => Some(previous.min(lag.unwrap_or(u64::MAX))),
3506 _ => lag,
3507 };
3508 result = CollectionReadiness::Lagging { lag };
3509 }
3510 CollectionReadiness::Unhydrated => {}
3511 }
3512 }
3513 result
3514}
3515
3516#[cfg(test)]
3517mod tests {
3518 use std::collections::BTreeSet;
3519
3520 use mz_compute_types::dyncfgs::{ENABLE_COLUMN_PAGED_BATCHER, ENABLE_MZ_JOIN_CORE};
3521 use mz_dyncfg::{ConfigSet, ConfigUpdates, ConfigVal};
3522 use mz_persist_types::PersistLocation;
3523 use mz_repr::{GlobalId, Timestamp};
3524 use timely::progress::Antichain;
3525 use tokio::sync::mpsc;
3526
3527 use crate::protocol::command::{ComputeCommand, InstanceConfig};
3528
3529 use super::{
3530 CollectionReadiness, Instance, ReplicaCollectionIntrospection, ReplicaCollectionState,
3531 ReplicaId, classify_collection_readiness,
3532 };
3533
3534 fn ac(ts: u64) -> Antichain<Timestamp> {
3535 Antichain::from_elem(Timestamp::new(ts))
3536 }
3537
3538 fn state(id: ReplicaId, as_of: u64, write: u64, output: u64) -> ReplicaCollectionState {
3539 let (tx, _rx) = mpsc::unbounded_channel();
3540 let introspection =
3541 ReplicaCollectionIntrospection::new(id, GlobalId::User(1), tx, ac(as_of));
3542 let mut state = ReplicaCollectionState::new(None, ac(as_of), introspection, Vec::new());
3543 state.update_write_frontier(ac(write));
3544 state.update_output_frontier(ac(output));
3545 state
3546 }
3547
3548 fn classify(
3549 replicas: &[(ReplicaId, ReplicaCollectionState)],
3550 targets: &[ReplicaId],
3551 references: &[ReplicaId],
3552 lag: Option<Timestamp>,
3553 ) -> CollectionReadiness {
3554 classify_collection_readiness(
3555 replicas.iter().map(|(id, state)| (*id, state)),
3556 &targets.iter().copied().collect::<BTreeSet<_>>(),
3557 &references.iter().copied().collect::<BTreeSet<_>>(),
3558 lag,
3559 )
3560 }
3561
3562 const LAG: Option<Timestamp> = Some(Timestamp::new(60));
3563
3564 #[mz_ore::test]
3568 fn shared_write_does_not_hide_output_lag() {
3569 let reference = ReplicaId::User(1);
3570 let target = ReplicaId::User(2);
3571 let mut replicas = vec![
3572 (reference, state(reference, 100, 10_000, 10_000)),
3573 (target, state(target, 100, 10_000, 101)),
3576 ];
3577 assert_eq!(
3578 classify(&replicas, &[target], &[reference], LAG),
3579 CollectionReadiness::Lagging { lag: Some(9_899) },
3580 );
3581 replicas[1].1.update_output_frontier(ac(9_940));
3582 assert_eq!(
3583 classify(&replicas, &[target], &[reference], LAG),
3584 CollectionReadiness::Ready,
3585 );
3586 }
3587
3588 #[mz_ore::test]
3589 fn refresh_writes_ahead_of_outputs_is_ready() {
3590 let reference = ReplicaId::User(1);
3591 let target = ReplicaId::User(2);
3592 let replicas = vec![
3593 (reference, state(reference, 100, 20_000, 10_000)),
3594 (target, state(target, 100, 20_000, 10_000)),
3595 ];
3596 assert_eq!(
3597 classify(&replicas, &[target], &[reference], LAG),
3598 CollectionReadiness::Ready,
3599 );
3600 }
3601
3602 #[mz_ore::test]
3603 fn furthest_reference_and_bystander_exclusion() {
3604 let trailing = ReplicaId::User(1);
3605 let ahead = ReplicaId::User(2);
3606 let target = ReplicaId::User(3);
3607 let bystander = ReplicaId::User(4);
3608 let replicas = vec![
3609 (trailing, state(trailing, 100, 9_000, 9_000)),
3610 (ahead, state(ahead, 100, 10_000, 10_000)),
3611 (target, state(target, 100, 8_990, 8_990)),
3612 (bystander, state(bystander, 100, 1_000_000, 1_000_000)),
3613 ];
3614 assert_eq!(
3615 classify(&replicas, &[target], &[trailing], LAG),
3616 CollectionReadiness::Ready,
3617 );
3618 assert_eq!(
3619 classify(&replicas, &[target], &[trailing, ahead], LAG),
3620 CollectionReadiness::Lagging { lag: Some(1_010) },
3621 );
3622 }
3623
3624 #[mz_ore::test]
3625 fn one_ready_target_suffices() {
3626 let reference = ReplicaId::User(1);
3627 let unhydrated = ReplicaId::User(2);
3628 let lagging = ReplicaId::User(3);
3629 let ready = ReplicaId::User(4);
3630 let replicas = vec![
3631 (reference, state(reference, 100, 10_000, 10_000)),
3632 (unhydrated, state(unhydrated, 10_000, 10_000, 10_000)),
3636 (lagging, state(lagging, 100, 1_000, 1_000)),
3637 (ready, state(ready, 100, 9_990, 9_990)),
3638 ];
3639 assert_eq!(
3640 classify(&replicas, &[unhydrated, lagging, ready], &[reference], LAG),
3641 CollectionReadiness::Ready,
3642 );
3643 assert_eq!(
3644 classify(&replicas, &[unhydrated, lagging], &[reference], LAG),
3645 CollectionReadiness::Lagging { lag: Some(9_000) },
3646 );
3647 }
3648
3649 #[mz_ore::test]
3650 fn no_lag_gate_is_hydration_only() {
3651 let reference = ReplicaId::User(1);
3652 let target = ReplicaId::User(2);
3653 let replicas = vec![
3654 (reference, state(reference, 100, 10_000, 10_000)),
3655 (target, state(target, 100, 101, 101)),
3656 ];
3657 assert_eq!(
3658 classify(&replicas, &[target], &[reference], None),
3659 CollectionReadiness::Ready,
3660 );
3661 let unhydrated = vec![(target, state(target, 100, 100, 100))];
3662 assert_eq!(
3663 classify(&unhydrated, &[target], &[], None),
3664 CollectionReadiness::Unhydrated,
3665 );
3666 }
3667
3668 #[mz_ore::test]
3669 fn lag_reports_the_closest_hydrated_target() {
3670 let reference = ReplicaId::User(1);
3671 let far = ReplicaId::User(2);
3672 let close = ReplicaId::User(3);
3673 let mut replicas = vec![
3674 (reference, state(reference, 100, 10_000, 10_000)),
3675 (far, state(far, 100, 1_000, 1_000)),
3676 (close, state(close, 100, 9_000, 9_000)),
3677 ];
3678 for _ in 0..2 {
3679 assert_eq!(
3680 classify(&replicas, &[far, close], &[reference], LAG),
3681 CollectionReadiness::Lagging { lag: Some(1_000) },
3682 );
3683 replicas.reverse();
3684 }
3685 }
3686
3687 #[mz_ore::test]
3688 fn completed_reference_requires_completed_target() {
3689 let reference = ReplicaId::User(1);
3690 let target = ReplicaId::User(2);
3691 let mut replicas = vec![
3692 (reference, state(reference, 100, 1_000, 1_000)),
3693 (target, state(target, 100, 1_000, 1_000)),
3694 ];
3695 replicas[0].1.update_output_frontier(Antichain::new());
3696 assert_eq!(
3697 classify(&replicas, &[target], &[reference], Some(Timestamp::new(0))),
3698 CollectionReadiness::Lagging { lag: None },
3699 );
3700 replicas[1].1.update_output_frontier(Antichain::new());
3701 assert_eq!(
3702 classify(&replicas, &[target], &[reference], Some(Timestamp::new(0))),
3703 CollectionReadiness::Ready,
3704 );
3705 }
3706
3707 #[mz_ore::test]
3708 fn no_reference_means_nothing_to_regress() {
3709 let target = ReplicaId::User(1);
3710 let replicas = vec![(target, state(target, 100, 5_000, 5_000))];
3711 assert_eq!(
3712 classify(&replicas, &[target], &[], Some(Timestamp::new(0))),
3713 CollectionReadiness::Ready,
3714 );
3715 assert_eq!(
3716 classify(&replicas, &[target], &[target], Some(Timestamp::new(0))),
3717 CollectionReadiness::Ready,
3718 );
3719 }
3720
3721 #[mz_ore::test]
3722 fn no_targets_is_unhydrated() {
3723 let reference = ReplicaId::User(1);
3724 let replicas = vec![(reference, state(reference, 100, 10_000, 10_000))];
3725 assert_eq!(
3726 classify(&replicas, &[], &[reference], LAG),
3727 CollectionReadiness::Unhydrated,
3728 );
3729 }
3730
3731 fn create_instance_command() -> ComputeCommand {
3732 ComputeCommand::CreateInstance(Box::new(InstanceConfig {
3733 logging: Default::default(),
3734 expiration_offset: None,
3735 peek_stash_persist_location: PersistLocation::new_in_mem(),
3736 arrangement_dictionary_compression: false,
3737 initial_config: Default::default(),
3738 }))
3739 }
3740
3741 fn initial_config(cmd: &ComputeCommand) -> &ConfigUpdates {
3742 match cmd {
3743 ComputeCommand::CreateInstance(config) => &config.initial_config,
3744 other => panic!("expected CreateInstance, got {other:?}"),
3745 }
3746 }
3747
3748 #[mz_ore::test]
3753 fn create_instance_snapshots_instance_wide_dyncfg() {
3754 let dyncfg = ConfigSet::default()
3755 .add(&ENABLE_COLUMN_PAGED_BATCHER)
3756 .add(&ENABLE_MZ_JOIN_CORE);
3757 let mut updates = ConfigUpdates::default();
3758 updates.add(&ENABLE_COLUMN_PAGED_BATCHER, true);
3759 updates.add(&ENABLE_MZ_JOIN_CORE, false);
3760 updates.apply(&dyncfg);
3761
3762 let cmd = Instance::specialize_command_for_replica(
3764 create_instance_command(),
3765 &ConfigUpdates::default(),
3766 &dyncfg,
3767 );
3768 let snapshot = initial_config(&cmd);
3769 assert_eq!(
3770 snapshot.updates.get(ENABLE_COLUMN_PAGED_BATCHER.name()),
3771 Some(&ConfigVal::Bool(true)),
3772 );
3773 assert_eq!(
3774 snapshot.updates.get(ENABLE_MZ_JOIN_CORE.name()),
3775 Some(&ConfigVal::Bool(false)),
3776 );
3777 }
3778
3779 #[mz_ore::test]
3782 fn create_instance_snapshot_applies_replica_override() {
3783 let dyncfg = ConfigSet::default().add(&ENABLE_COLUMN_PAGED_BATCHER);
3784 let mut updates = ConfigUpdates::default();
3785 updates.add(&ENABLE_COLUMN_PAGED_BATCHER, true);
3786 updates.apply(&dyncfg);
3787
3788 let mut override_updates = ConfigUpdates::default();
3789 override_updates.add(&ENABLE_COLUMN_PAGED_BATCHER, false);
3790
3791 let cmd = Instance::specialize_command_for_replica(
3792 create_instance_command(),
3793 &override_updates,
3794 &dyncfg,
3795 );
3796 assert_eq!(
3797 initial_config(&cmd)
3798 .updates
3799 .get(ENABLE_COLUMN_PAGED_BATCHER.name()),
3800 Some(&ConfigVal::Bool(false)),
3801 "replica override should win over the instance-wide value",
3802 );
3803 }
3804
3805 #[mz_ore::test]
3807 fn update_configuration_merges_replica_override() {
3808 let dyncfg = ConfigSet::default().add(&ENABLE_COLUMN_PAGED_BATCHER);
3809
3810 let mut override_updates = ConfigUpdates::default();
3811 override_updates.add(&ENABLE_COLUMN_PAGED_BATCHER, true);
3812
3813 let cmd = Instance::specialize_command_for_replica(
3814 ComputeCommand::UpdateConfiguration(Box::new(Default::default())),
3815 &override_updates,
3816 &dyncfg,
3817 );
3818 match cmd {
3819 ComputeCommand::UpdateConfiguration(params) => assert_eq!(
3820 params
3821 .dyncfg_updates
3822 .updates
3823 .get(ENABLE_COLUMN_PAGED_BATCHER.name()),
3824 Some(&ConfigVal::Bool(true)),
3825 ),
3826 other => panic!("expected UpdateConfiguration, got {other:?}"),
3827 }
3828 }
3829}