1use std::collections::{BTreeMap, BTreeSet};
29use std::sync::Arc;
30use std::time::Duration;
31
32use mz_adapter_types::dyncfgs::CLUSTER_CONTROLLER_TICK_INTERVAL;
33use mz_catalog::memory::objects::{ClusterConfig, ClusterVariant};
34use mz_cluster_controller::ClusterController;
35use mz_cluster_controller::ctx::{
36 ApplyOutcome, AvailabilityZones, ClusterControllerCtx, ClusterState, CreateReason, Decision,
37 ExpectedClusterState, ObservedReplica, OnTimeout, ReconfigurationRecord, ReconfigurationStatus,
38 ReconfigurationTarget, RefreshMvInfo, RefreshWindowClusterInputs, RefreshWindowInputsBatch,
39 ReplicaShape, StateWrite,
40};
41use mz_compute_types::config::ComputeReplicaConfig;
42use mz_controller::clusters::ClusterStatus;
43use mz_controller_types::{ClusterId, ReplicaId};
44use mz_ore::task::spawn;
45use mz_repr::Timestamp;
46use tokio::sync::{mpsc, oneshot};
47use tracing::{debug, warn};
48
49use crate::catalog::{DropObjectInfo, Op, ReplicaCreateDropReason};
50use crate::coord::{ClusterReplicaStatuses, Coordinator, Message};
51use crate::error::AdapterError;
52
53#[derive(Debug)]
61pub enum ClusterControllerRequest {
62 ManagedClusterIds { tx: oneshot::Sender<Vec<ClusterId>> },
64 ClusterStates {
67 clusters: Vec<ClusterId>,
68 tx: oneshot::Sender<(Vec<ClusterState>, Timestamp)>,
69 },
70 HydratedReplicas {
73 cluster_id: ClusterId,
74 replicas: Vec<ReplicaId>,
75 tx: oneshot::Sender<BTreeSet<ReplicaId>>,
76 },
77 HasHydratableObjects {
80 cluster_id: ClusterId,
81 tx: oneshot::Sender<bool>,
82 },
83 RefreshWindowClusterInputs {
86 cluster_id: ClusterId,
87 tx: oneshot::Sender<Option<RefreshWindowClusterInputs>>,
88 },
89 RefreshWindowReadTs { tx: oneshot::Sender<Timestamp> },
91 Apply {
93 decisions: Vec<Decision>,
94 tx: oneshot::Sender<ApplyOutcome>,
95 },
96 TickInterval { tx: oneshot::Sender<Duration> },
100}
101
102struct ReplicaHydrationCheck {
103 replica_id: ReplicaId,
104 compute_hydrated: oneshot::Receiver<bool>,
105}
106
107struct CoordCtx {
110 internal_cmd_tx: mpsc::UnboundedSender<Message>,
111 now: Timestamp,
115}
116
117impl CoordCtx {
118 async fn request<T>(
121 &self,
122 make: impl FnOnce(oneshot::Sender<T>) -> ClusterControllerRequest,
123 ) -> Option<T> {
124 let (tx, rx) = oneshot::channel();
125 if self
126 .internal_cmd_tx
127 .send(Message::ClusterControllerRequest(make(tx)))
128 .is_err()
129 {
130 return None;
131 }
132 rx.await.ok()
133 }
134}
135
136#[async_trait::async_trait]
137impl ClusterControllerCtx for CoordCtx {
138 fn now(&self) -> Timestamp {
139 self.now
140 }
141
142 async fn managed_cluster_ids(&mut self) -> Vec<ClusterId> {
143 self.request(|tx| ClusterControllerRequest::ManagedClusterIds { tx })
144 .await
145 .unwrap_or_default()
146 }
147
148 async fn cluster_states(&mut self, clusters: &[ClusterId]) -> Vec<ClusterState> {
149 let clusters = clusters.to_vec();
150 match self
151 .request(|tx| ClusterControllerRequest::ClusterStates { clusters, tx })
152 .await
153 {
154 Some((states, now)) => {
155 self.now = now;
156 states
157 }
158 None => Vec::new(),
159 }
160 }
161
162 async fn hydrated_replicas(
163 &mut self,
164 cluster_id: ClusterId,
165 replicas: &[ReplicaId],
166 ) -> BTreeSet<ReplicaId> {
167 let replicas = replicas.to_vec();
168 self.request(|tx| ClusterControllerRequest::HydratedReplicas {
169 cluster_id,
170 replicas,
171 tx,
172 })
173 .await
174 .unwrap_or_default()
175 }
176
177 async fn has_hydratable_objects(&mut self, cluster_id: ClusterId) -> bool {
178 self.request(|tx| ClusterControllerRequest::HasHydratableObjects { cluster_id, tx })
179 .await
180 .unwrap_or(false)
183 }
184
185 async fn refresh_window_inputs(
186 &mut self,
187 cluster_ids: &[ClusterId],
188 ) -> Option<RefreshWindowInputsBatch> {
189 let mut cluster_inputs = BTreeMap::new();
190 for (index, &cluster_id) in cluster_ids.iter().enumerate() {
191 if index > 0 {
192 tokio::task::yield_now().await;
196 }
197 let inputs = self
198 .request(|tx| ClusterControllerRequest::RefreshWindowClusterInputs {
199 cluster_id,
200 tx,
201 })
202 .await
203 .flatten();
204 if let Some(inputs) = inputs {
205 cluster_inputs.insert(cluster_id, inputs);
206 }
207 }
208 if cluster_inputs.is_empty() {
209 return None;
210 }
211
212 let read_ts = self
213 .request(|tx| ClusterControllerRequest::RefreshWindowReadTs { tx })
214 .await?;
215 Some(RefreshWindowInputsBatch {
216 read_ts,
217 cluster_inputs,
218 })
219 }
220
221 async fn apply(&mut self, decisions: Vec<Decision>) -> ApplyOutcome {
222 self.request(|tx| ClusterControllerRequest::Apply { decisions, tx })
223 .await
224 .unwrap_or(ApplyOutcome::Rejected)
227 }
228}
229
230impl Coordinator {
231 pub(crate) fn spawn_cluster_controller_task(&self) {
244 let internal_cmd_tx = self.internal_cmd_tx.clone();
245 let reconcile_now = Arc::clone(&self.reconcile_now);
246 let dyncfgs = self.catalog().system_config().dyncfgs().clone();
249
250 spawn(|| "cluster_controller", async move {
251 let controller = ClusterController::new(dyncfgs);
252 let mut ctx = CoordCtx {
253 internal_cmd_tx,
254 now: Timestamp::MIN,
255 };
256
257 loop {
258 let Some(interval) = ctx
261 .request(|tx| ClusterControllerRequest::TickInterval { tx })
262 .await
263 else {
264 break;
265 };
266 tokio::select! {
267 _ = tokio::time::sleep(interval.max(Duration::from_millis(1))) => {}
268 _ = reconcile_now.notified() => {}
269 }
270
271 if ctx.internal_cmd_tx.is_closed() {
272 break;
274 }
275 controller.reconcile(&mut ctx).await;
276 }
277 });
278 }
279
280 #[mz_ore::instrument(level = "debug")]
291 pub(crate) async fn handle_cluster_controller_request(
292 &mut self,
293 request: ClusterControllerRequest,
294 ) {
295 let active = !self.controller.read_only();
296
297 match request {
298 ClusterControllerRequest::ManagedClusterIds { tx } => {
299 let ids = if active {
300 self.catalog()
301 .clusters()
302 .filter(|c| c.is_managed())
303 .map(|c| c.id)
304 .collect()
305 } else {
306 Vec::new()
307 };
308 let _ = tx.send(ids);
309 }
310 ClusterControllerRequest::ClusterStates { clusters, tx } => {
311 let now = Timestamp::from(self.now());
312 let states: Vec<_> = clusters
316 .into_iter()
317 .filter_map(|id| self.observe_cluster_state(id))
318 .collect();
319 let _ = tx.send((states, now));
320 }
321 ClusterControllerRequest::HydratedReplicas {
322 cluster_id,
323 replicas,
324 tx,
325 } => {
326 let checks = self.start_hydration_checks(cluster_id, replicas);
327 spawn(|| "cluster_controller_hydration_probe", async move {
331 let mut hydrated = BTreeSet::new();
332 for check in checks {
333 if check.compute_hydrated.await.unwrap_or(false) {
334 hydrated.insert(check.replica_id);
335 }
336 }
337 let _ = tx.send(hydrated);
338 });
339 }
340 ClusterControllerRequest::HasHydratableObjects { cluster_id, tx } => {
341 let _ = tx.send(self.cluster_has_hydratable_objects(cluster_id));
342 }
343 ClusterControllerRequest::RefreshWindowClusterInputs { cluster_id, tx } => {
344 let _ = tx.send(self.refresh_window_catalog_inputs(cluster_id));
345 }
346 ClusterControllerRequest::RefreshWindowReadTs { tx } => {
347 let oracle = self.get_local_timestamp_oracle();
351 spawn(|| "cluster_controller_refresh_window_read_ts", async move {
352 let read_ts = oracle.read_ts().await;
353 let _ = tx.send(read_ts);
354 });
355 }
356 ClusterControllerRequest::Apply { decisions, tx } => {
357 let outcome = if active {
358 self.apply_cluster_decisions(decisions).await
359 } else {
360 ApplyOutcome::Rejected
361 };
362 let _ = tx.send(outcome);
363 }
364 ClusterControllerRequest::TickInterval { tx } => {
365 let interval =
366 CLUSTER_CONTROLLER_TICK_INTERVAL.get(self.catalog().system_config().dyncfgs());
367 let _ = tx.send(interval);
368 }
369 }
370 }
371
372 fn observe_cluster_state(&self, cluster_id: ClusterId) -> Option<ClusterState> {
375 let cluster = self.catalog().try_get_cluster(cluster_id)?;
376 let ClusterVariant::Managed(managed) = &cluster.config.variant else {
377 return None;
378 };
379 let expected = crate::catalog::cluster_state::project_expected(managed);
383
384 let replicas = cluster
387 .replicas()
388 .map(|replica| ObservedReplica {
389 replica_id: replica.replica_id,
390 name: replica.name.clone(),
391 shape: replica_shape(&replica.config),
392 internal: replica.config.location.internal(),
393 billed_as: replica.config.location.billed_as().is_some(),
394 pending: replica.config.location.pending(),
395 })
396 .collect();
397
398 Some(ClusterState {
399 cluster_id,
400 size: expected.size,
401 replication_factor: expected.replication_factor,
402 availability_zones: expected.availability_zones.0,
403 logging: expected.logging,
404 arrangement_compression: expected.arrangement_compression,
405 schedule: expected.schedule,
406 auto_scaling_policy: expected.auto_scaling_policy,
407 reconfiguration: expected.reconfiguration,
408 burst: expected.burst,
409 replicas,
410 })
411 }
412
413 fn cluster_has_hydratable_objects(&self, cluster_id: ClusterId) -> bool {
418 let Some(cluster) = self.catalog().try_get_cluster(cluster_id) else {
419 return false;
420 };
421 cluster
422 .bound_objects
423 .iter()
424 .any(|id| self.catalog().get_entry(id).item().is_hydratable())
425 }
426
427 fn start_hydration_checks(
433 &self,
434 cluster_id: ClusterId,
435 replicas: Vec<ReplicaId>,
436 ) -> Vec<ReplicaHydrationCheck> {
437 use mz_catalog::memory::objects::CatalogItem;
438
439 let pinned_mvs: Vec<(ReplicaId, mz_repr::GlobalId)> = self
449 .catalog()
450 .try_get_cluster(cluster_id)
451 .into_iter()
452 .flat_map(|cluster| cluster.bound_objects.iter())
453 .filter_map(|id| match self.catalog().get_entry(id).item() {
454 CatalogItem::MaterializedView(mv) => mv
455 .target_replica
456 .map(|target| (target, mv.global_id_writes())),
457 _ => None,
458 })
459 .collect();
460
461 let mut checks = Vec::new();
462 for replica_id in replicas {
463 let status = self
467 .cluster_replica_statuses
468 .try_get_cluster_replica_statuses(cluster_id, replica_id)
469 .map(ClusterReplicaStatuses::cluster_replica_status);
470 if !matches!(status, Some(ClusterStatus::Online)) {
471 continue;
472 }
473 let exclude: BTreeSet<mz_repr::GlobalId> = pinned_mvs
474 .iter()
475 .filter(|(target, _)| *target != replica_id)
476 .map(|(_, id)| *id)
477 .collect();
478 let compute_fut = match self.controller.compute.collections_hydrated_for_replicas(
479 cluster_id,
480 vec![replica_id],
481 exclude.clone(),
482 ) {
483 Ok(fut) => fut,
484 Err(_) => continue,
487 };
488 let storage_hydrated = match self.controller.storage.collections_hydrated_on_replicas(
489 Some(vec![replica_id]),
490 &cluster_id,
491 &exclude,
492 ) {
493 Ok(hydrated) => hydrated,
494 Err(_) => continue,
495 };
496 if storage_hydrated {
497 checks.push(ReplicaHydrationCheck {
498 replica_id,
499 compute_hydrated: compute_fut,
500 });
501 }
502 }
503 checks
504 }
505
506 fn refresh_window_catalog_inputs(
521 &self,
522 cluster_id: ClusterId,
523 ) -> Option<RefreshWindowClusterInputs> {
524 use mz_catalog::memory::objects::CatalogItem;
525
526 let cluster = self.catalog().try_get_cluster(cluster_id)?;
527 let ClusterVariant::Managed(managed) = &cluster.config.variant else {
528 return None;
529 };
530 if !matches!(
531 managed.schedule,
532 mz_sql::plan::ClusterSchedule::Refresh { .. }
533 ) {
534 return None;
535 }
536
537 let refresh_mvs = cluster
538 .bound_objects
539 .iter()
540 .filter_map(|id| {
541 let CatalogItem::MaterializedView(mv) = self.catalog().get_entry(id).item() else {
542 return None;
543 };
544 let refresh_schedule = mv.refresh_schedule.clone()?;
545 let (_since, write_frontier) = self
549 .controller
550 .storage
551 .collection_frontiers(mv.global_id_writes())
552 .expect("storage controller knows about catalog MVs");
553 Some(RefreshMvInfo {
554 id: mv.global_id_writes(),
555 write_frontier,
556 refresh_schedule,
557 })
558 })
559 .collect();
560
561 let compaction_estimate = self
562 .catalog()
563 .system_config()
564 .cluster_refresh_mv_compaction_estimate();
565
566 Some(RefreshWindowClusterInputs {
567 compaction_estimate,
568 refresh_mvs,
569 })
570 }
571
572 async fn apply_cluster_decisions(&mut self, decisions: Vec<Decision>) -> ApplyOutcome {
583 let checks = Self::partition_checks(&decisions);
584
585 let Some(replica_ids) = self.allocate_replica_ids_for_creates(&decisions).await else {
588 return ApplyOutcome::Rejected;
589 };
590
591 let Some(mutations) = self.build_mutation_ops(decisions, replica_ids) else {
592 return ApplyOutcome::Rejected;
593 };
594 if mutations.is_empty() {
595 return ApplyOutcome::Applied;
599 }
600
601 self.commit_with_checks(checks, mutations).await
602 }
603
604 fn partition_checks(decisions: &[Decision]) -> Vec<(ClusterId, ExpectedClusterState)> {
609 let mut checks: Vec<(ClusterId, ExpectedClusterState)> = Vec::new();
610 let mut seen_clusters = BTreeSet::new();
611 for decision in decisions {
612 let (cluster_id, expected) = match decision {
613 Decision::CreateReplica {
614 cluster_id,
615 expected,
616 ..
617 }
618 | Decision::DropReplica {
619 cluster_id,
620 expected,
621 ..
622 }
623 | Decision::UpdateClusterState {
624 cluster_id,
625 expected,
626 ..
627 } => (*cluster_id, expected),
628 };
629 if seen_clusters.insert(cluster_id) {
630 checks.push((cluster_id, expected.clone()));
631 } else {
632 debug_assert!(
633 checks
634 .iter()
635 .any(|(c, e)| *c == cluster_id && e == expected),
636 "decisions for a cluster in one tick must share one expected witness",
637 );
638 }
639 }
640 checks
641 }
642
643 async fn allocate_replica_ids_for_creates(
652 &mut self,
653 decisions: &[Decision],
654 ) -> Option<Vec<ReplicaId>> {
655 let mut replica_ids = Vec::new();
656 for decision in decisions {
657 let Decision::CreateReplica { cluster_id, .. } = decision else {
658 continue;
659 };
660 let id_ts = self.get_catalog_write_ts().await;
661 let result = self
662 .catalog()
663 .allocate_replica_ids(*cluster_id, 1, id_ts)
664 .await;
665 match result {
666 Ok(ids) => {
667 replica_ids.push(ids.into_iter().next().expect("allocated one replica id"))
668 }
669 Err(err) => {
670 warn!(%cluster_id, "cluster controller could not allocate replica id: {err}");
671 return None;
672 }
673 }
674 }
675 Some(replica_ids)
676 }
677
678 fn build_mutation_ops(
683 &self,
684 decisions: Vec<Decision>,
685 replica_ids: Vec<ReplicaId>,
686 ) -> Option<Vec<Op>> {
687 let mut replica_ids = replica_ids.into_iter();
688 let mut mutations = Vec::new();
689 let mut drops = Vec::new();
690 for decision in decisions {
691 match decision {
692 Decision::UpdateClusterState {
693 cluster_id, write, ..
694 } => match self.build_update_cluster_config_op(cluster_id, &write) {
695 Some(op) => mutations.push(op),
696 None => return None,
698 },
699 Decision::CreateReplica {
700 cluster_id,
701 name,
702 shape,
703 reason,
704 ..
705 } => {
706 let replica_id = replica_ids.next().expect("one pre-allocated id per create");
707 let reason = audit_reason_for_create(reason);
708 match self.build_create_replica_op(cluster_id, replica_id, name, &shape, reason)
709 {
710 Ok(Some(op)) => mutations.push(op),
711 Ok(None) => return None,
712 Err(err) => {
713 warn!(%cluster_id, "cluster controller could not build replica create: {err}");
714 return None;
715 }
716 }
717 }
718 Decision::DropReplica {
719 cluster_id,
720 replica_id,
721 ..
722 } => {
723 if self
730 .catalog()
731 .try_get_cluster_replica(cluster_id, replica_id)
732 .is_none()
733 {
734 return None;
735 }
736 drops.push(DropObjectInfo::ClusterReplica((
737 cluster_id,
738 replica_id,
739 ReplicaCreateDropReason::Retired,
740 )));
741 }
742 }
743 }
744 if !drops.is_empty() {
745 mutations.push(Op::DropObjects(drops));
746 }
747 Some(mutations)
748 }
749
750 async fn commit_with_checks(
761 &mut self,
762 checks: Vec<(ClusterId, ExpectedClusterState)>,
763 mutations: Vec<Op>,
764 ) -> ApplyOutcome {
765 let mut ops: Vec<Op> = checks
766 .into_iter()
767 .map(|(cluster_id, expected)| Op::CheckClusterState {
768 cluster_id,
769 expected,
770 })
771 .collect();
772 ops.extend(mutations);
773
774 match self.catalog_transact(None, ops).await {
775 Ok(()) => ApplyOutcome::Applied,
776 Err(AdapterError::ClusterStateChanged { .. }) => {
777 ApplyOutcome::Rejected
781 }
782 Err(AdapterError::ReadOnly) => {
783 debug!("cluster controller apply skipped in read-only mode");
788 ApplyOutcome::Rejected
789 }
790 Err(AdapterError::ResourceExhaustion { .. }) => {
791 debug!("cluster controller apply exceeded the resource budget");
794 ApplyOutcome::ResourceExhausted
795 }
796 Err(err) => {
797 warn!("cluster controller apply failed: {err}");
798 ApplyOutcome::Rejected
799 }
800 }
801 }
802
803 fn build_update_cluster_config_op(
808 &self,
809 cluster_id: ClusterId,
810 write: &StateWrite,
811 ) -> Option<Op> {
812 let cluster = self.catalog().try_get_cluster(cluster_id)?;
813 let mut config = cluster.config.clone();
814 let ClusterConfig {
815 variant: ClusterVariant::Managed(managed),
816 ..
817 } = &mut config
818 else {
819 return None;
820 };
821 let StateWrite {
827 new_size,
828 new_replication_factor,
829 new_availability_zones,
830 new_logging,
831 new_arrangement_compression,
832 reconfiguration,
833 burst,
834 } = write;
835 if let Some(size) = new_size {
836 managed.size = size.clone();
837 }
838 if let Some(rf) = new_replication_factor {
839 managed.replication_factor = *rf;
840 }
841 if let Some(azs) = new_availability_zones {
842 managed.availability_zones = azs.clone();
843 }
844 if let Some(logging) = new_logging {
845 managed.logging = logging.clone();
846 }
847 if let Some(arrangement_compression) = new_arrangement_compression {
848 managed.arrangement_compression = *arrangement_compression;
849 }
850 if let Some(reconfiguration) = reconfiguration {
851 managed.reconfiguration = reconfiguration.record.as_ref().map(memory_reconfiguration);
852 }
853 if let Some(burst) = burst {
854 managed.burst = burst.record.as_ref().map(memory_burst);
855 }
856 let reconfiguration_audit = write.reconfiguration.as_ref().and_then(|w| w.audit);
860 let burst_audit = write.burst.as_ref().and_then(|w| w.audit);
861 Some(Op::UpdateClusterConfig {
862 id: cluster_id,
863 name: cluster.name.clone(),
864 config,
865 reconfiguration_audit,
866 burst_audit,
867 })
868 }
869
870 fn build_create_replica_op(
874 &self,
875 cluster_id: ClusterId,
876 replica_id: ReplicaId,
877 name: String,
878 shape: &ReplicaShape,
879 reason: ReplicaCreateDropReason,
880 ) -> Result<Option<Op>, mz_catalog::memory::error::Error> {
881 let Some(cluster) = self.catalog().try_get_cluster(cluster_id) else {
882 return Ok(None);
883 };
884 if !cluster.is_managed() {
885 return Ok(None);
886 }
887 let owner_id = cluster.owner_id;
888
889 let location = mz_catalog::durable::ReplicaLocation::Managed {
890 availability_zones: Vec::new(),
892 billed_as: None,
893 internal: false,
894 size: shape.size.clone(),
895 pending: false,
896 };
897 let azs: Option<&[String]> = if shape.availability_zones.0.is_empty() {
898 None
899 } else {
900 Some(&shape.availability_zones.0)
901 };
902 let location = self.catalog().concretize_replica_location(
903 location,
904 &self
905 .catalog()
906 .get_role_allowed_cluster_sizes(&Some(owner_id)),
907 azs,
908 false,
909 )?;
910
911 let config = mz_controller::clusters::ReplicaConfig {
912 location,
913 compute: ComputeReplicaConfig {
914 logging: shape.logging.clone(),
915 arrangement_compression: shape.arrangement_compression,
916 },
917 };
918
919 Ok(Some(Op::CreateClusterReplica {
920 cluster_id,
921 replica_id,
922 name,
923 config,
924 owner_id,
925 reason,
926 }))
927 }
928}
929
930fn audit_reason_for_create(reason: CreateReason) -> ReplicaCreateDropReason {
940 match reason {
941 CreateReason::Baseline => ReplicaCreateDropReason::Manual,
942 CreateReason::GracefulReconfiguration => ReplicaCreateDropReason::GracefulReconfiguration,
943 CreateReason::HydrationBurst => ReplicaCreateDropReason::HydrationBurst,
944 CreateReason::OnRefresh(decision) => ReplicaCreateDropReason::OnRefresh(decision),
945 }
946}
947
948fn replica_shape(config: &mz_controller::clusters::ReplicaConfig) -> Option<ReplicaShape> {
951 use mz_controller::clusters::ReplicaLocation;
952 let ReplicaLocation::Managed(managed) = &config.location else {
953 return None;
954 };
955 Some(ReplicaShape {
956 size: managed.size.clone(),
957 availability_zones: AvailabilityZones(managed.availability_zones.clone()),
958 logging: config.compute.logging.clone(),
959 arrangement_compression: config.compute.arrangement_compression,
960 })
961}
962
963fn on_timeout_from_controller(action: OnTimeout) -> mz_sql::plan::OnTimeoutAction {
964 match action {
965 OnTimeout::Commit => mz_sql::plan::OnTimeoutAction::Commit,
966 OnTimeout::Rollback => mz_sql::plan::OnTimeoutAction::Rollback,
967 }
968}
969
970fn memory_reconfiguration(
971 record: &ReconfigurationRecord,
972) -> mz_catalog::memory::objects::ReconfigurationState {
973 let ReconfigurationRecord {
976 target,
977 deadline,
978 on_timeout,
979 status,
980 } = record;
981 let ReconfigurationTarget {
982 size,
983 replication_factor,
984 availability_zones,
985 logging,
986 arrangement_compression,
987 } = target;
988 mz_catalog::memory::objects::ReconfigurationState {
989 target: mz_catalog::memory::objects::ReconfigurationTarget {
990 size: size.clone(),
991 replication_factor: *replication_factor,
992 availability_zones: availability_zones.0.clone(),
993 logging: logging.clone(),
994 arrangement_compression: *arrangement_compression,
995 },
996 deadline: *deadline,
997 on_timeout: on_timeout_from_controller(*on_timeout),
998 status: status_from_controller(*status),
999 }
1000}
1001
1002fn status_from_controller(
1003 status: ReconfigurationStatus,
1004) -> mz_catalog::memory::objects::ReconfigurationStatus {
1005 match status {
1006 ReconfigurationStatus::InProgress => {
1007 mz_catalog::memory::objects::ReconfigurationStatus::InProgress
1008 }
1009 ReconfigurationStatus::Finalized => {
1010 mz_catalog::memory::objects::ReconfigurationStatus::Finalized
1011 }
1012 ReconfigurationStatus::TimedOut => {
1013 mz_catalog::memory::objects::ReconfigurationStatus::TimedOut
1014 }
1015 ReconfigurationStatus::Cancelled => {
1016 mz_catalog::memory::objects::ReconfigurationStatus::Cancelled
1017 }
1018 ReconfigurationStatus::ResourceExhausted => {
1019 mz_catalog::memory::objects::ReconfigurationStatus::ResourceExhausted
1020 }
1021 }
1022}
1023
1024fn memory_burst(
1025 record: &mz_cluster_controller::ctx::BurstRecord,
1026) -> mz_catalog::memory::objects::BurstState {
1027 let mz_cluster_controller::ctx::BurstRecord {
1030 burst_size,
1031 linger_duration,
1032 steady_hydrated_at,
1033 } = record;
1034 mz_catalog::memory::objects::BurstState {
1035 burst_size: burst_size.clone(),
1036 linger_duration: *linger_duration,
1037 steady_hydrated_at: *steady_hydrated_at,
1038 }
1039}
1040
1041#[cfg(test)]
1042mod tests {
1043 use super::*;
1044
1045 #[mz_ore::test]
1046 fn test_audit_reason_for_create() {
1047 use ReplicaCreateDropReason as Reason;
1048 use mz_cluster_controller::ctx::RefreshWindowDecision;
1049 use mz_repr::GlobalId;
1050
1051 assert!(matches!(
1054 audit_reason_for_create(CreateReason::Baseline),
1055 Reason::Manual
1056 ));
1057 assert!(matches!(
1058 audit_reason_for_create(CreateReason::GracefulReconfiguration),
1059 Reason::GracefulReconfiguration
1060 ));
1061 assert!(matches!(
1062 audit_reason_for_create(CreateReason::HydrationBurst),
1063 Reason::HydrationBurst
1064 ));
1065
1066 let decision = RefreshWindowDecision {
1069 objects_needing_refresh: vec![GlobalId::User(1)],
1070 objects_needing_compaction: vec![GlobalId::User(2)],
1071 hydration_time_estimate: Duration::from_secs(7),
1072 };
1073 match audit_reason_for_create(CreateReason::OnRefresh(decision.clone())) {
1074 Reason::OnRefresh(carried) => assert_eq!(carried, decision),
1075 other => panic!("expected an on-refresh reason, got {other:?}"),
1076 }
1077 }
1078}