1use std::collections::{BTreeMap, BTreeSet};
11use std::time::{Duration, Instant};
12
13use itertools::Itertools;
14use maplit::btreeset;
15use mz_adapter_types::cluster_state::ReconfigurationAudit;
16use mz_catalog::builtin::BUILTINS;
17use mz_catalog::memory::objects::{
18 ClusterConfig, ClusterReplica, ClusterVariant, ClusterVariantManaged,
19 ManagedReplicaConfigShape, ReconfigurationState, ReconfigurationStatus, ReconfigurationTarget,
20};
21use mz_compute_types::config::ComputeReplicaConfig;
22use mz_controller::clusters::{
23 ManagedReplicaLocation, ReplicaConfig, ReplicaLocation, ReplicaLogging,
24};
25use mz_controller_types::{ClusterId, DEFAULT_REPLICA_LOGGING_INTERVAL, ReplicaId};
26use mz_ore::cast::CastFrom;
27use mz_ore::collections::CollectionExt;
28use mz_ore::instrument;
29use mz_repr::adt::numeric::Numeric;
30use mz_repr::role_id::RoleId;
31use mz_sql::ast::{Ident, QualifiedReplica};
32use mz_sql::catalog::{CatalogCluster, ObjectType};
33use mz_sql::plan::{
34 self, AlterClusterPlanStrategy, AlterClusterRenamePlan, AlterClusterReplicaRenamePlan,
35 AlterClusterSwapPlan, AlterOptionParameter, AlterSetClusterPlan,
36 ComputeReplicaIntrospectionConfig, CreateClusterManagedPlan, CreateClusterPlan,
37 CreateClusterReplicaPlan, CreateClusterUnmanagedPlan, CreateClusterVariant, PlanClusterOption,
38};
39use mz_sql::plan::{AlterClusterPlan, OnTimeoutAction};
40use mz_sql::session::metadata::SessionMetadata;
41use mz_sql::session::vars::{
42 MAX_CREDIT_CONSUMPTION_RATE, MAX_REPLICAS_PER_CLUSTER, SystemVars, Var,
43};
44use tracing::{Instrument, Span, debug};
45
46use mz_adapter_types::dyncfgs::{
47 DEFAULT_CLUSTER_RECONFIGURATION_TIMEOUT, ENABLE_BACKGROUND_ALTER_CLUSTER,
48 ENABLE_CLUSTER_CONTROLLER,
49};
50
51use super::return_if_err;
52use crate::AdapterError::AlterClusterWhilePendingReplicas;
53use crate::catalog::{self, Op, ReplicaCreateDropReason};
54use crate::config::{
55 ClusterEvalContext, ClusterScopeContext, ReplicaEvalContext, ReplicaScopeContext,
56};
57use crate::coord::{
58 AlterCluster, AlterClusterAwaitReconfiguration, AlterClusterFinalize,
59 AlterClusterWaitForHydrated, ClusterStage, Coordinator, Message, PlanValidity, StageResult,
60 Staged,
61};
62use crate::{AdapterError, ExecuteContext, ExecuteResponse, session::Session};
63
64const PENDING_REPLICA_SUFFIX: &str = "-pending";
65
66impl Staged for ClusterStage {
67 type Ctx = ExecuteContext;
68
69 fn validity(&mut self) -> &mut PlanValidity {
70 match self {
71 Self::Alter(stage) => &mut stage.validity,
72 Self::WaitForHydrated(stage) => &mut stage.validity,
73 Self::Finalize(stage) => &mut stage.validity,
74 Self::AwaitReconfiguration(stage) => &mut stage.validity,
75 }
76 }
77
78 async fn stage(
79 self,
80 coord: &mut Coordinator,
81 ctx: &mut ExecuteContext,
82 ) -> Result<StageResult<Box<Self>>, crate::AdapterError> {
83 match self {
84 Self::Alter(stage) => {
85 coord
86 .sequence_alter_cluster_stage(ctx.session(), stage.plan.clone(), stage.validity)
87 .await
88 }
89 Self::WaitForHydrated(stage) => {
90 let AlterClusterWaitForHydrated {
91 validity,
92 plan,
93 new_config,
94 workload_class,
95 timeout_time,
96 on_timeout,
97 } = stage;
98 coord
99 .check_if_pending_replicas_hydrated_stage(
100 ctx.session(),
101 plan,
102 new_config,
103 workload_class,
104 timeout_time,
105 on_timeout,
106 validity,
107 )
108 .await
109 }
110 Self::Finalize(stage) => {
111 coord
112 .finalize_alter_cluster_stage(
113 ctx.session(),
114 stage.plan.clone(),
115 stage.new_config.clone(),
116 stage.workload_class.clone(),
117 )
118 .await
119 }
120 Self::AwaitReconfiguration(stage) => {
121 coord.await_reconfiguration_stage(stage.validity, stage.cluster_id, stage.target)
122 }
123 }
124 }
125
126 fn message(self, ctx: ExecuteContext, span: tracing::Span) -> Message {
127 Message::ClusterStageReady {
128 ctx,
129 span,
130 stage: self,
131 }
132 }
133
134 fn cancel_enabled(&self) -> bool {
135 true
136 }
137}
138
139impl Coordinator {
140 #[instrument]
141 pub(crate) async fn sequence_alter_cluster_staged(
142 &mut self,
143 ctx: ExecuteContext,
144 plan: plan::AlterClusterPlan,
145 ) {
146 let stage = return_if_err!(self.alter_cluster_validate(ctx.session(), plan).await, ctx);
147 self.sequence_staged(ctx, Span::current(), stage).await;
148 }
149
150 #[instrument]
151 async fn alter_cluster_validate(
152 &self,
153 session: &Session,
154 plan: plan::AlterClusterPlan,
155 ) -> Result<ClusterStage, AdapterError> {
156 let validity = PlanValidity::new(
157 self.catalog(),
158 BTreeSet::new(),
159 Some(plan.id.clone()),
160 None,
161 session.role_metadata().clone(),
162 );
163 Ok(ClusterStage::Alter(AlterCluster { validity, plan }))
164 }
165
166 async fn sequence_alter_cluster_stage(
167 &mut self,
168 session: &Session,
169 plan: plan::AlterClusterPlan,
170 validity: PlanValidity,
171 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
172 let AlterClusterPlan {
173 id: cluster_id,
174 name: _,
175 ref options,
176 ref strategy,
177 } = plan;
178
179 use mz_catalog::memory::objects::ClusterVariant::*;
180 use mz_sql::plan::AlterOptionParameter::*;
181 let cluster = self.catalog.get_cluster(cluster_id);
182 let config = cluster.config.clone();
183 let mut new_config = config.clone();
184
185 match (&new_config.variant, &options.managed) {
186 (Managed(_), Reset) | (Managed(_), Unchanged) | (Managed(_), Set(true)) => {}
187 (Managed(_), Set(false)) => new_config.variant = Unmanaged,
188 (Unmanaged, Unchanged) | (Unmanaged, Set(false)) => {}
189 (Unmanaged, Reset) | (Unmanaged, Set(true)) => {
190 let size = "".to_string();
194 let logging = ReplicaLogging {
195 log_logging: false,
196 interval: Some(DEFAULT_REPLICA_LOGGING_INTERVAL),
197 };
198 new_config.variant = Managed(ClusterVariantManaged {
199 size,
200 availability_zones: Default::default(),
201 logging,
202 replication_factor: 1,
203 optimizer_feature_overrides: Default::default(),
204 schedule: Default::default(),
205 auto_scaling_strategy: None,
206 reconfiguration: None,
207 burst: None,
208 });
209 }
210 }
211
212 match &mut new_config.variant {
213 Managed(ClusterVariantManaged {
214 size,
215 availability_zones,
216 logging,
217 replication_factor,
218 optimizer_feature_overrides: _,
219 schedule,
220 auto_scaling_strategy: _,
221 reconfiguration: _,
222 burst: _,
223 }) => {
224 match &options.size {
225 Set(s) => size.clone_from(s),
226 Reset => coord_bail!("SIZE has no default value"),
227 Unchanged => {}
228 }
229 match &options.availability_zones {
230 Set(az) => availability_zones.clone_from(az),
231 Reset => *availability_zones = Default::default(),
232 Unchanged => {}
233 }
234 match &options.introspection_debugging {
235 Set(id) => logging.log_logging = *id,
236 Reset => logging.log_logging = false,
237 Unchanged => {}
238 }
239 match &options.introspection_interval {
240 Set(ii) => logging.interval = ii.0,
241 Reset => logging.interval = Some(DEFAULT_REPLICA_LOGGING_INTERVAL),
242 Unchanged => {}
243 }
244 match &options.replication_factor {
245 Set(rf) => *replication_factor = *rf,
246 Reset => {
247 *replication_factor = self
248 .catalog
249 .system_config()
250 .default_cluster_replication_factor()
251 }
252 Unchanged => {}
253 }
254 match &options.schedule {
255 Set(new_schedule) => {
256 *schedule = new_schedule.clone();
257 }
258 Reset => *schedule = Default::default(),
259 Unchanged => {}
260 }
261 if !matches!(options.replicas, Unchanged) {
262 coord_bail!("Cannot change REPLICAS of managed clusters");
263 }
264 }
265 Unmanaged => {
266 if !matches!(options.size, Unchanged) {
267 coord_bail!("Cannot change SIZE of unmanaged clusters");
268 }
269 if !matches!(options.availability_zones, Unchanged) {
270 coord_bail!("Cannot change AVAILABILITY ZONES of unmanaged clusters");
271 }
272 if !matches!(options.introspection_debugging, Unchanged) {
273 coord_bail!("Cannot change INTROSPECTION DEGUBBING of unmanaged clusters");
274 }
275 if !matches!(options.introspection_interval, Unchanged) {
276 coord_bail!("Cannot change INTROSPECTION INTERVAL of unmanaged clusters");
277 }
278 if !matches!(options.replication_factor, Unchanged) {
279 coord_bail!("Cannot change REPLICATION FACTOR of unmanaged clusters");
280 }
281 }
282 }
283
284 match &options.workload_class {
285 Set(wc) => new_config.workload_class.clone_from(wc),
286 Reset => new_config.workload_class = None,
287 Unchanged => {}
288 }
289
290 let cluster_controller_owns = ENABLE_CLUSTER_CONTROLLER
297 .get(self.catalog().system_config().dyncfgs())
298 && cluster_id.is_user();
299 let reconfiguration_in_flight = matches!(
300 &config.variant,
301 Managed(managed) if managed
302 .reconfiguration
303 .as_ref()
304 .is_some_and(|record| record.is_in_progress())
305 );
306
307 if cluster_controller_owns
314 && reconfiguration_in_flight
315 && !matches!(options.replication_factor, Unchanged)
316 {
317 return Err(AdapterError::AlterClusterReplicationFactorWhileReconfiguring);
318 }
319
320 let cancels_or_retargets =
325 reconfiguration_in_flight && alter_changes_replica_shape(options);
326 if new_config == config && !(cluster_controller_owns && cancels_or_retargets) {
327 return Ok(StageResult::Response(ExecuteResponse::AlteredObject(
328 ObjectType::Cluster,
329 )));
330 }
331
332 if cluster_controller_owns {
344 if let (Managed(old_managed), Managed(new_managed)) =
345 (&config.variant, &new_config.variant)
346 {
347 let needs_record = if reconfiguration_in_flight {
348 alter_changes_replica_shape(options)
349 } else {
350 new_managed.replica_config_shape() != old_managed.replica_config_shape()
351 };
352 if needs_record {
353 return self
354 .reshape_alter_cluster_managed(
355 session,
356 cluster_id,
357 new_config.clone(),
358 options,
359 strategy,
360 validity,
361 )
362 .await;
363 }
364 }
365 }
366
367 match (&config.variant, &new_config.variant) {
368 (Managed(_), Managed(new_config_managed)) => {
369 let alter_followup = self
370 .sequence_alter_cluster_managed_to_managed(
371 Some(session),
372 cluster_id,
373 new_config.clone(),
374 ReplicaCreateDropReason::Manual,
375 strategy.clone(),
376 )
377 .await?;
378 if alter_followup == NeedsFinalization::Yes {
379 self.active_conns
382 .get_mut(session.conn_id())
383 .expect("There must be an active connection")
384 .pending_cluster_alters
385 .insert(cluster_id.clone());
386 let new_config_managed = new_config_managed.clone();
387 return match &strategy {
388 AlterClusterPlanStrategy::None => Err(AdapterError::Internal(
389 "AlterClusterPlanStrategy must not be None if NeedsFinalization is Yes"
390 .into(),
391 )),
392 AlterClusterPlanStrategy::For(duration) => {
393 let span = Span::current();
394 let plan = plan.clone();
395 let duration = duration.clone().to_owned();
396 let workload_class = new_config.workload_class.clone();
397 Ok(StageResult::Handle(mz_ore::task::spawn(
398 || "Finalize Alter Cluster",
399 async move {
400 tokio::time::sleep(duration).await;
401 let stage = ClusterStage::Finalize(AlterClusterFinalize {
402 validity,
403 plan,
404 new_config: new_config_managed,
405 workload_class,
406 });
407 Ok(Box::new(stage))
408 }
409 .instrument(span),
410 )))
411 }
412 AlterClusterPlanStrategy::UntilReady {
413 timeout,
414 on_timeout,
415 } => Ok(StageResult::Immediate(Box::new(
416 ClusterStage::WaitForHydrated(AlterClusterWaitForHydrated {
417 validity,
418 plan: plan.clone(),
419 new_config: new_config_managed.clone(),
420 workload_class: new_config.workload_class.clone(),
421 timeout_time: Instant::now() + timeout.to_owned(),
422 on_timeout: on_timeout.unwrap_or(OnTimeoutAction::Commit),
426 }),
427 ))),
428 };
429 }
430 }
431 (Unmanaged, Managed(_)) => {
432 self.sequence_alter_cluster_unmanaged_to_managed(
433 session,
434 cluster_id,
435 new_config,
436 options.to_owned(),
437 )
438 .await?;
439 }
440 (Managed(_), Unmanaged) => {
441 self.sequence_alter_cluster_managed_to_unmanaged(session, cluster_id, new_config)
442 .await?;
443 }
444 (Unmanaged, Unmanaged) => {
445 self.sequence_alter_cluster_unmanaged_to_unmanaged(
446 session,
447 cluster_id,
448 new_config,
449 options.replicas.clone(),
450 )
451 .await?;
452 }
453 }
454
455 Ok(StageResult::Response(ExecuteResponse::AlteredObject(
456 ObjectType::Cluster,
457 )))
458 }
459
460 fn validate_reconfiguration_resource_limits(
462 &self,
463 cluster_id: ClusterId,
464 target: &ReconfigurationTarget,
465 ) -> Result<(), AdapterError> {
466 if !cluster_id.is_user() {
469 return Ok(());
470 }
471 let cluster = self.catalog().get_cluster(cluster_id);
472 let ClusterVariant::Managed(realized) = &cluster.config.variant else {
473 return Ok(());
474 };
475
476 if target.matches_realized_config(realized) {
482 return Ok(());
483 }
484
485 self.validate_resource_limit(
500 usize::cast_from(realized.replication_factor),
501 i64::from(target.replication_factor),
502 SystemVars::max_replicas_per_cluster,
503 "cluster replica",
504 MAX_REPLICAS_PER_CLUSTER.name(),
505 )?;
506
507 self.validate_reconfiguration_credit_peak(cluster_id, realized, target)?;
509
510 Ok(())
511 }
512
513 fn validate_reconfiguration_credit_peak(
521 &self,
522 cluster_id: ClusterId,
523 realized: &ClusterVariantManaged,
524 target: &ReconfigurationTarget,
525 ) -> Result<(), AdapterError> {
526 let shape_credit = |size: &str, replication_factor: u32| -> Numeric {
527 let per_replica = self
528 .catalog()
529 .cluster_replica_sizes()
530 .0
531 .get(size)
532 .map(|allocation| allocation.credits_per_hour)
533 .unwrap_or_else(Numeric::zero);
536 per_replica * Numeric::from(replication_factor)
537 };
538 let mut peak_credit = shape_credit(&target.size, target.replication_factor);
539 peak_credit += shape_credit(&realized.size, realized.replication_factor);
540 self.validate_resource_limit_numeric(
541 self.current_credit_consumption_rate(Some(cluster_id)),
542 peak_credit,
543 |system_vars| {
544 self.license_key
545 .max_credit_consumption_rate()
546 .map_or_else(|| system_vars.max_credit_consumption_rate(), Numeric::from)
547 },
548 "cluster replica",
549 MAX_CREDIT_CONSUMPTION_RATE.name(),
550 )?;
551
552 Ok(())
553 }
554
555 async fn reshape_alter_cluster_managed(
591 &mut self,
592 session: &Session,
593 cluster_id: ClusterId,
594 new_config: ClusterConfig,
595 options: &PlanClusterOption,
596 strategy: &AlterClusterPlanStrategy,
597 validity: PlanValidity,
598 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
599 use mz_sql::plan::AlterOptionParameter::Unchanged;
600
601 let ClusterVariant::Managed(new_managed) = &new_config.variant else {
602 return Err(AdapterError::Internal(
603 "reshape_alter_cluster_managed requires a managed target config".into(),
604 ));
605 };
606
607 let cluster = self.catalog.get_cluster(cluster_id);
613 let in_flight = match &cluster.config.variant {
614 ClusterVariant::Managed(managed) => managed
615 .reconfiguration
616 .as_ref()
617 .filter(|record| record.is_in_progress())
618 .cloned(),
619 ClusterVariant::Unmanaged => None,
620 };
621 let new_target = ReconfigurationTarget {
622 size: new_managed.size.clone(),
623 replication_factor: new_managed.replication_factor,
624 availability_zones: new_managed.availability_zones.clone(),
625 logging: new_managed.logging.clone(),
626 };
627 let unchanged = ReconfigurationDimensionsUnchanged {
628 size: matches!(options.size, Unchanged),
629 replication_factor: matches!(options.replication_factor, Unchanged),
630 availability_zones: matches!(options.availability_zones, Unchanged),
631 log_logging: matches!(options.introspection_debugging, Unchanged),
634 interval: matches!(options.introspection_interval, Unchanged),
635 };
636 let target = fold_reconfiguration_target(
637 in_flight.as_ref().map(|r| &r.target),
638 new_target,
639 unchanged,
640 );
641
642 let role_id = session.role_metadata().current_role;
645 self.catalog.ensure_valid_replica_size(
646 &self
647 .catalog()
648 .get_role_allowed_cluster_sizes(&Some(role_id)),
649 &target.size,
650 false,
651 )?;
652 self.ensure_valid_azs(target.availability_zones.iter())?;
653 self.validate_reconfiguration_resource_limits(cluster_id, &target)?;
657
658 let (timeout, on_timeout) = match strategy {
678 AlterClusterPlanStrategy::None => (
679 DEFAULT_CLUSTER_RECONFIGURATION_TIMEOUT
680 .get(self.catalog().system_config().dyncfgs()),
681 OnTimeoutAction::Rollback,
682 ),
683 AlterClusterPlanStrategy::For(timeout) => (*timeout, OnTimeoutAction::Commit),
684 AlterClusterPlanStrategy::UntilReady {
685 timeout,
686 on_timeout,
687 } => (*timeout, on_timeout.unwrap_or(OnTimeoutAction::Rollback)),
688 };
689 let deadline = self
690 .now()
691 .saturating_add(u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX));
692
693 let cluster = self.catalog.get_cluster(cluster_id);
702 let cluster_name = cluster.name().to_string();
703 let ClusterVariant::Managed(realized_now) = &cluster.config.variant else {
704 return Err(AdapterError::Internal(
705 "reshape_alter_cluster_managed requires a managed realized config".into(),
706 ));
707 };
708 let realized_size = realized_now.size.clone();
709 let realized_replication_factor = realized_now.replication_factor;
710 let realized_availability_zones = realized_now.availability_zones.clone();
711 let realized_logging = realized_now.logging.clone();
712 let (status, audit) = if target.matches_realized_config(realized_now) {
716 (
717 ReconfigurationStatus::Cancelled,
718 ReconfigurationAudit::Cancelled,
719 )
720 } else {
721 (
722 ReconfigurationStatus::InProgress,
723 ReconfigurationAudit::Started,
724 )
725 };
726 let record = ReconfigurationState {
727 target: target.clone(),
728 deadline: deadline.into(),
729 on_timeout,
730 status,
731 };
732
733 let mut realized = new_config.clone();
734 let ClusterVariant::Managed(realized_managed) = &mut realized.variant else {
735 return Err(AdapterError::Internal(
736 "reshape_alter_cluster_managed requires a managed target config".into(),
737 ));
738 };
739 realized_managed.size = realized_size;
740 realized_managed.replication_factor = realized_replication_factor;
741 realized_managed.availability_zones = realized_availability_zones;
742 realized_managed.logging = realized_logging;
743 realized_managed.reconfiguration = Some(record);
744
745 self.catalog_transact(
746 Some(session),
747 vec![Op::UpdateClusterConfig {
748 id: cluster_id,
749 name: cluster_name,
750 config: realized,
751 reconfiguration_audit: Some(audit),
752 burst_audit: None,
753 }],
754 )
755 .await?;
756
757 let background =
758 ENABLE_BACKGROUND_ALTER_CLUSTER.get(self.catalog().system_config().dyncfgs());
759 if background {
760 return Ok(StageResult::Response(ExecuteResponse::AlteredObject(
761 ObjectType::Cluster,
762 )));
763 }
764
765 Ok(StageResult::Immediate(Box::new(
769 ClusterStage::AwaitReconfiguration(AlterClusterAwaitReconfiguration {
770 validity,
771 cluster_id,
772 target,
773 }),
774 )))
775 }
776
777 fn await_reconfiguration_stage(
783 &self,
784 validity: PlanValidity,
785 cluster_id: ClusterId,
786 target: ReconfigurationTarget,
787 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
788 let Some(cluster) = self.catalog().try_get_cluster(cluster_id) else {
789 return Ok(StageResult::Response(ExecuteResponse::AlteredObject(
792 ObjectType::Cluster,
793 )));
794 };
795 let record = match &cluster.config.variant {
796 ClusterVariant::Managed(managed) => managed.reconfiguration.clone(),
797 ClusterVariant::Unmanaged => None,
798 };
799
800 let realized_matches_target = match &cluster.config.variant {
801 ClusterVariant::Managed(managed) => target.matches_realized_config(managed),
802 ClusterVariant::Unmanaged => false,
803 };
804
805 match record {
806 None => {
807 if realized_matches_target {
810 Ok(StageResult::Response(ExecuteResponse::AlteredObject(
811 ObjectType::Cluster,
812 )))
813 } else {
814 Err(AdapterError::AlterClusterTimeout)
815 }
816 }
817 Some(record) if !record.is_in_progress() => {
818 if matches!(
819 record.status,
820 ReconfigurationStatus::Finalized | ReconfigurationStatus::Cancelled
821 ) && realized_matches_target
822 {
823 Ok(StageResult::Response(ExecuteResponse::AlteredObject(
824 ObjectType::Cluster,
825 )))
826 } else {
827 Err(AdapterError::AlterClusterTimeout)
828 }
829 }
830 Some(_) => {
831 let poll_duration = self
841 .catalog
842 .system_config()
843 .cluster_alter_check_ready_interval();
844 let span = Span::current();
845 Ok(StageResult::Handle(mz_ore::task::spawn(
846 || "Await Cluster Reconfiguration",
847 async move {
848 tokio::time::sleep(poll_duration).await;
849 Ok(Box::new(ClusterStage::AwaitReconfiguration(
850 AlterClusterAwaitReconfiguration {
851 validity,
852 cluster_id,
853 target,
854 },
855 )))
856 }
857 .instrument(span),
858 )))
859 }
860 }
861 }
862
863 async fn finalize_alter_cluster_stage(
864 &mut self,
865 session: &Session,
866 AlterClusterPlan {
867 id: cluster_id,
868 name: cluster_name,
869 ..
870 }: AlterClusterPlan,
871 new_config: ClusterVariantManaged,
872 workload_class: Option<String>,
873 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
874 let cluster = self.catalog.get_cluster(cluster_id);
875 let mut ops = vec![];
876
877 let remove_replicas = cluster
880 .replicas()
881 .filter_map(|r| {
882 if !r.config.location.pending() && !r.config.location.internal() {
883 Some(catalog::DropObjectInfo::ClusterReplica((
884 cluster_id.clone(),
885 r.replica_id,
886 ReplicaCreateDropReason::Manual,
887 )))
888 } else {
889 None
890 }
891 })
892 .collect();
893 ops.push(catalog::Op::DropObjects(remove_replicas));
894
895 let finalize_replicas: Vec<catalog::Op> = cluster
898 .replicas()
899 .filter_map(|r| {
900 if r.config.location.pending() {
901 let cluster_ident = match Ident::new(cluster.name.clone()) {
902 Ok(id) => id,
903 Err(err) => {
904 return Some(Err(AdapterError::internal(
905 "Unexpected error parsing cluster name",
906 err,
907 )));
908 }
909 };
910 let replica_ident = match Ident::new(r.name.clone()) {
911 Ok(id) => id,
912 Err(err) => {
913 return Some(Err(AdapterError::internal(
914 "Unexpected error parsing replica name",
915 err,
916 )));
917 }
918 };
919 Some(Ok((cluster_ident, replica_ident, r)))
920 } else {
921 None
922 }
923 })
924 .collect::<Result<Vec<(Ident, Ident, &ClusterReplica)>, _>>()?
927 .into_iter()
928 .map(|(cluster_ident, replica_ident, replica)| {
929 let mut new_replica_config = replica.config.clone();
930 debug!("Promoting replica: {}", replica.name);
931 match new_replica_config.location {
932 mz_controller::clusters::ReplicaLocation::Managed(ManagedReplicaLocation {
933 ref mut pending,
934 ..
935 }) => {
936 *pending = false;
937 }
938 mz_controller::clusters::ReplicaLocation::Unmanaged(_) => {}
939 }
940
941 let mut replica_ops = vec![];
942 let to_name = replica.name.strip_suffix(PENDING_REPLICA_SUFFIX);
943 if let Some(to_name) = to_name {
944 replica_ops.push(catalog::Op::RenameClusterReplica {
945 cluster_id: cluster_id.clone(),
946 replica_id: replica.replica_id.to_owned(),
947 name: QualifiedReplica {
948 cluster: cluster_ident,
949 replica: replica_ident,
950 },
951 to_name: to_name.to_owned(),
952 });
953 }
954 replica_ops.push(catalog::Op::UpdateClusterReplicaConfig {
955 cluster_id,
956 replica_id: replica.replica_id.to_owned(),
957 config: new_replica_config,
958 });
959 replica_ops
960 })
961 .flatten()
962 .collect();
963
964 ops.extend(finalize_replicas);
965
966 let mut final_config = ClusterConfig {
970 variant: ClusterVariant::Managed(new_config),
971 workload_class: workload_class.clone(),
972 };
973 let reconfiguration_audit = cancel_carried_reconfiguration(&mut final_config);
974 ops.push(Op::UpdateClusterConfig {
975 id: cluster_id,
976 name: cluster_name,
977 config: final_config,
978 reconfiguration_audit,
979 burst_audit: None,
980 });
981 self.catalog_transact(Some(session), ops).await?;
982 self.active_conns
985 .get_mut(session.conn_id())
986 .expect("There must be an active connection")
987 .pending_cluster_alters
988 .remove(&cluster_id);
989
990 Ok(StageResult::Response(ExecuteResponse::AlteredObject(
991 ObjectType::Cluster,
992 )))
993 }
994
995 async fn check_if_pending_replicas_hydrated_stage(
996 &mut self,
997 session: &Session,
998 plan: AlterClusterPlan,
999 new_config: ClusterVariantManaged,
1000 workload_class: Option<String>,
1001 timeout_time: Instant,
1002 on_timeout: OnTimeoutAction,
1003 validity: PlanValidity,
1004 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
1005 let cluster = self.catalog.get_cluster(plan.id);
1007 let pending_replicas = cluster
1008 .replicas()
1009 .filter_map(|r| {
1010 if r.config.location.pending() {
1011 Some(r.replica_id.clone())
1012 } else {
1013 None
1014 }
1015 })
1016 .collect_vec();
1017 if Instant::now() > timeout_time {
1019 match on_timeout {
1021 OnTimeoutAction::Rollback => {
1022 self.active_conns
1023 .get_mut(session.conn_id())
1024 .expect("There must be an active connection")
1025 .pending_cluster_alters
1026 .remove(&cluster.id);
1027 self.drop_reconfiguration_replicas(btreeset!(cluster.id))
1028 .await?;
1029 return Err(AdapterError::AlterClusterTimeout);
1030 }
1031 OnTimeoutAction::Commit => {
1032 let span = Span::current();
1033 let poll_duration = self
1034 .catalog
1035 .system_config()
1036 .cluster_alter_check_ready_interval()
1037 .clone();
1038 return Ok(StageResult::Handle(mz_ore::task::spawn(
1039 || "Finalize Alter Cluster",
1040 async move {
1041 tokio::time::sleep(poll_duration).await;
1042 let stage = ClusterStage::Finalize(AlterClusterFinalize {
1043 validity,
1044 plan,
1045 new_config,
1046 workload_class,
1047 });
1048 Ok(Box::new(stage))
1049 }
1050 .instrument(span),
1051 )));
1052 }
1053 }
1054 }
1055 let compute_hydrated_fut = self
1056 .controller
1057 .compute
1058 .collections_hydrated_for_replicas(cluster.id, pending_replicas.clone(), [].into())
1059 .map_err(|e| AdapterError::internal("Failed to check hydration", e))?;
1060
1061 let storage_hydrated = self
1062 .controller
1063 .storage
1064 .collections_hydrated_on_replicas(Some(pending_replicas), &cluster.id, &[].into())
1065 .map_err(|e| AdapterError::internal("Failed to check hydration", e))?;
1066
1067 let span = Span::current();
1068 Ok(StageResult::Handle(mz_ore::task::spawn(
1069 || "Alter Cluster: wait for hydrated",
1070 async move {
1071 let compute_hydrated = compute_hydrated_fut
1072 .await
1073 .map_err(|e| AdapterError::internal("Failed to check hydration", e))?;
1074
1075 if compute_hydrated && storage_hydrated {
1076 Ok(Box::new(ClusterStage::Finalize(AlterClusterFinalize {
1078 validity,
1079 plan,
1080 new_config: new_config.clone(),
1081 workload_class: workload_class.clone(),
1082 })))
1083 } else {
1084 tokio::time::sleep(Duration::from_secs(1)).await;
1086 let stage = ClusterStage::WaitForHydrated(AlterClusterWaitForHydrated {
1087 validity,
1088 plan,
1089 new_config,
1090 workload_class,
1091 timeout_time,
1092 on_timeout,
1093 });
1094 Ok(Box::new(stage))
1095 }
1096 }
1097 .instrument(span),
1098 )))
1099 }
1100
1101 #[mz_ore::instrument(level = "debug")]
1102 pub(crate) async fn sequence_create_cluster(
1103 &mut self,
1104 session: &Session,
1105 CreateClusterPlan {
1106 name,
1107 variant,
1108 workload_class,
1109 }: CreateClusterPlan,
1110 ) -> Result<ExecuteResponse, AdapterError> {
1111 tracing::debug!("sequence_create_cluster");
1112
1113 let id_ts = self.get_catalog_write_ts().await;
1114 let id = self.catalog().allocate_user_cluster_id(id_ts).await?;
1115 let introspection_sources = BUILTINS::logs().collect();
1120 let cluster_variant = match &variant {
1121 CreateClusterVariant::Managed(plan) => {
1122 let logging = if let Some(config) = plan.compute.introspection {
1123 ReplicaLogging {
1124 log_logging: config.debugging,
1125 interval: Some(config.interval),
1126 }
1127 } else {
1128 ReplicaLogging::default()
1129 };
1130 ClusterVariant::Managed(ClusterVariantManaged {
1131 size: plan.size.clone(),
1132 availability_zones: plan.availability_zones.clone(),
1133 logging,
1134 replication_factor: plan.replication_factor,
1135 optimizer_feature_overrides: plan.optimizer_feature_overrides.clone(),
1136 schedule: plan.schedule.clone(),
1137 auto_scaling_strategy: None,
1138 reconfiguration: None,
1139 burst: None,
1140 })
1141 }
1142 CreateClusterVariant::Unmanaged(_) => ClusterVariant::Unmanaged,
1143 };
1144 let config = ClusterConfig {
1145 variant: cluster_variant,
1146 workload_class,
1147 };
1148 let ops = vec![catalog::Op::CreateCluster {
1149 id,
1150 name: name.clone(),
1151 introspection_sources,
1152 owner_id: *session.current_role_id(),
1153 config,
1154 }];
1155
1156 match variant {
1157 CreateClusterVariant::Managed(plan) => {
1158 self.sequence_create_managed_cluster(session, plan, id, name, ops)
1159 .await
1160 }
1161 CreateClusterVariant::Unmanaged(plan) => {
1162 self.sequence_create_unmanaged_cluster(session, plan, id, name, ops)
1163 .await
1164 }
1165 }
1166 }
1167
1168 #[mz_ore::instrument(level = "debug")]
1169 async fn sequence_create_managed_cluster(
1170 &mut self,
1171 session: &Session,
1172 CreateClusterManagedPlan {
1173 availability_zones,
1174 compute,
1175 replication_factor,
1176 size,
1177 optimizer_feature_overrides: _,
1178 schedule: _,
1179 }: CreateClusterManagedPlan,
1180 cluster_id: ClusterId,
1181 cluster_name: String,
1182 mut ops: Vec<catalog::Op>,
1183 ) -> Result<ExecuteResponse, AdapterError> {
1184 tracing::debug!("sequence_create_managed_cluster");
1185
1186 self.ensure_valid_azs(availability_zones.iter())?;
1187
1188 let role_id = session.role_metadata().current_role;
1189 self.catalog.ensure_valid_replica_size(
1190 &self
1191 .catalog()
1192 .get_role_allowed_cluster_sizes(&Some(role_id)),
1193 &size,
1194 false,
1195 )?;
1196
1197 if cluster_id.is_user() {
1202 self.validate_resource_limit(
1203 0,
1204 i64::from(replication_factor),
1205 SystemVars::max_replicas_per_cluster,
1206 "cluster replica",
1207 MAX_REPLICAS_PER_CLUSTER.name(),
1208 )?;
1209 }
1210
1211 let id_ts = self.get_catalog_write_ts().await;
1217 let replica_ids = self
1218 .catalog()
1219 .allocate_replica_ids(cluster_id, u64::from(replication_factor), id_ts)
1220 .await?;
1221
1222 let cluster_ctx = ClusterScopeContext {
1223 id: cluster_id.to_string(),
1224 name: cluster_name.clone(),
1225 is_builtin: cluster_id.is_system(),
1226 };
1227
1228 let mut replica_ctxs = Vec::new();
1229 for (replica_id, replica_name) in replica_ids
1230 .into_iter()
1231 .zip_eq((0..replication_factor).map(managed_cluster_replica_name))
1232 {
1233 let size_family = self.create_managed_cluster_replica_op(
1234 cluster_id,
1235 replica_id,
1236 replica_name.clone(),
1237 &compute,
1238 &size,
1239 &mut ops,
1240 if availability_zones.is_empty() {
1241 None
1242 } else {
1243 Some(availability_zones.as_ref())
1244 },
1245 false,
1246 *session.current_role_id(),
1247 ReplicaCreateDropReason::Manual,
1248 )?;
1249 replica_ctxs.push(ReplicaEvalContext {
1250 cluster_id,
1251 replica_id,
1252 cluster: cluster_ctx.clone(),
1253 replica: ReplicaScopeContext {
1254 id: replica_id.to_string(),
1255 name: replica_name,
1256 is_builtin: cluster_id.is_system(),
1257 size: size.clone(),
1258 size_family,
1259 cluster_id: cluster_id.to_string(),
1260 cluster_name: cluster_name.clone(),
1261 },
1262 });
1263 }
1264
1265 let cluster_eval = ClusterEvalContext {
1272 cluster_id,
1273 cluster: cluster_ctx,
1274 };
1275 if let Some(scoped_op) = self.scoped_overrides_create_op(&[cluster_eval], &replica_ctxs) {
1276 ops.push(scoped_op);
1277 }
1278
1279 self.catalog_transact(Some(session), ops).await?;
1280
1281 Ok(ExecuteResponse::CreatedCluster)
1282 }
1283
1284 fn create_managed_cluster_replica_op(
1285 &self,
1286 cluster_id: ClusterId,
1287 replica_id: ReplicaId,
1288 name: String,
1289 compute: &mz_sql::plan::ComputeReplicaConfig,
1290 size: &String,
1291 ops: &mut Vec<Op>,
1292 azs: Option<&[String]>,
1293 pending: bool,
1294 owner_id: RoleId,
1295 reason: ReplicaCreateDropReason,
1296 ) -> Result<String, AdapterError> {
1297 let location = mz_catalog::durable::ReplicaLocation::Managed {
1298 availability_zones: Vec::new(),
1301 billed_as: None,
1302 internal: false,
1303 size: size.clone(),
1304 pending,
1305 };
1306
1307 let logging = if let Some(config) = compute.introspection {
1308 ReplicaLogging {
1309 log_logging: config.debugging,
1310 interval: Some(config.interval),
1311 }
1312 } else {
1313 ReplicaLogging::default()
1314 };
1315
1316 let config = ReplicaConfig {
1317 location: self.catalog().concretize_replica_location(
1318 location,
1319 &self
1320 .catalog()
1321 .get_role_allowed_cluster_sizes(&Some(owner_id)),
1322 azs,
1323 false,
1324 )?,
1325 compute: ComputeReplicaConfig { logging },
1326 };
1327
1328 let size_family = match &config.location {
1334 ReplicaLocation::Managed(location) => location.allocation.family().to_string(),
1335 ReplicaLocation::Unmanaged(_) => {
1337 unreachable!("managed cluster replica has a managed location")
1338 }
1339 };
1340
1341 ops.push(catalog::Op::CreateClusterReplica {
1342 cluster_id,
1343 replica_id,
1344 name,
1345 config,
1346 owner_id,
1347 reason,
1348 });
1349 Ok(size_family)
1350 }
1351
1352 fn ensure_valid_azs<'a, I: IntoIterator<Item = &'a String>>(
1353 &self,
1354 azs: I,
1355 ) -> Result<(), AdapterError> {
1356 let cat_azs = self.catalog().state().availability_zones();
1357 for az in azs.into_iter() {
1358 if !cat_azs.contains(az) {
1359 return Err(AdapterError::InvalidClusterReplicaAz {
1360 az: az.to_string(),
1361 expected: cat_azs.to_vec(),
1362 });
1363 }
1364 }
1365 Ok(())
1366 }
1367
1368 #[mz_ore::instrument(level = "debug")]
1369 async fn sequence_create_unmanaged_cluster(
1370 &mut self,
1371 session: &Session,
1372 CreateClusterUnmanagedPlan { replicas }: CreateClusterUnmanagedPlan,
1373 id: ClusterId,
1374 cluster_name: String,
1375 mut ops: Vec<catalog::Op>,
1376 ) -> Result<ExecuteResponse, AdapterError> {
1377 tracing::debug!("sequence_create_unmanaged_cluster");
1378
1379 self.ensure_valid_azs(replicas.iter().filter_map(|(_, r)| {
1380 if let mz_sql::plan::ReplicaConfig::Orchestrated {
1381 availability_zone: Some(az),
1382 ..
1383 } = &r
1384 {
1385 Some(az)
1386 } else {
1387 None
1388 }
1389 }))?;
1390
1391 if id.is_user() {
1396 self.validate_resource_limit(
1397 0,
1398 i64::try_from(replicas.len()).unwrap_or(i64::MAX),
1399 SystemVars::max_replicas_per_cluster,
1400 "cluster replica",
1401 MAX_REPLICAS_PER_CLUSTER.name(),
1402 )?;
1403 }
1404
1405 let id_ts = self.get_catalog_write_ts().await;
1411 let replica_ids = self
1412 .catalog()
1413 .allocate_replica_ids(id, u64::cast_from(replicas.len()), id_ts)
1414 .await?;
1415
1416 let cluster_ctx = ClusterScopeContext {
1417 id: id.to_string(),
1418 name: cluster_name.clone(),
1419 is_builtin: id.is_system(),
1420 };
1421 let mut replica_ctxs = Vec::new();
1422
1423 for (replica_id, (replica_name, replica_config)) in replica_ids.into_iter().zip_eq(replicas)
1424 {
1425 let (compute, location) = match replica_config {
1428 mz_sql::plan::ReplicaConfig::Unorchestrated {
1429 storagectl_addrs,
1430 computectl_addrs,
1431 compute,
1432 } => {
1433 let location = mz_catalog::durable::ReplicaLocation::Unmanaged {
1434 storagectl_addrs,
1435 computectl_addrs,
1436 };
1437 (compute, location)
1438 }
1439 mz_sql::plan::ReplicaConfig::Orchestrated {
1440 availability_zone,
1441 billed_as,
1442 compute,
1443 internal,
1444 size,
1445 } => {
1446 if !session.user().is_internal() && (internal || billed_as.is_some()) {
1448 coord_bail!("cannot specify INTERNAL or BILLED AS as non-internal user")
1449 }
1450 if billed_as.is_some() && !internal {
1452 coord_bail!("must specify INTERNAL when specifying BILLED AS");
1453 }
1454
1455 let location = mz_catalog::durable::ReplicaLocation::Managed {
1456 availability_zones: availability_zone.into_iter().collect(),
1459 billed_as,
1460 internal,
1461 size: size.clone(),
1462 pending: false,
1463 };
1464 (compute, location)
1465 }
1466 };
1467
1468 let logging = if let Some(config) = compute.introspection {
1469 ReplicaLogging {
1470 log_logging: config.debugging,
1471 interval: Some(config.interval),
1472 }
1473 } else {
1474 ReplicaLogging::default()
1475 };
1476
1477 let role_id = session.role_metadata().current_role;
1478 let config = ReplicaConfig {
1479 location: self.catalog().concretize_replica_location(
1480 location,
1481 &self
1482 .catalog()
1483 .get_role_allowed_cluster_sizes(&Some(role_id)),
1484 None,
1485 false,
1486 )?,
1487 compute: ComputeReplicaConfig { logging },
1488 };
1489
1490 if let ReplicaLocation::Managed(location) = &config.location {
1493 replica_ctxs.push(ReplicaEvalContext {
1494 cluster_id: id,
1495 replica_id,
1496 cluster: cluster_ctx.clone(),
1497 replica: ReplicaScopeContext {
1498 id: replica_id.to_string(),
1499 name: replica_name.clone(),
1500 is_builtin: id.is_system(),
1501 size: location.size.clone(),
1502 size_family: location.allocation.family().to_string(),
1503 cluster_id: id.to_string(),
1504 cluster_name: cluster_name.clone(),
1505 },
1506 });
1507 }
1508
1509 ops.push(catalog::Op::CreateClusterReplica {
1510 cluster_id: id,
1511 replica_id,
1512 name: replica_name.clone(),
1513 config,
1514 owner_id: *session.current_role_id(),
1515 reason: ReplicaCreateDropReason::Manual,
1516 });
1517 }
1518
1519 let cluster_eval = ClusterEvalContext {
1522 cluster_id: id,
1523 cluster: cluster_ctx,
1524 };
1525 if let Some(scoped_op) = self.scoped_overrides_create_op(&[cluster_eval], &replica_ctxs) {
1526 ops.push(scoped_op);
1527 }
1528
1529 self.catalog_transact(Some(session), ops).await?;
1530
1531 Ok(ExecuteResponse::CreatedCluster)
1532 }
1533
1534 #[mz_ore::instrument(level = "debug")]
1535 pub(crate) async fn sequence_create_cluster_replica(
1536 &mut self,
1537 session: &Session,
1538 CreateClusterReplicaPlan {
1539 name,
1540 cluster_id,
1541 config,
1542 }: CreateClusterReplicaPlan,
1543 ) -> Result<ExecuteResponse, AdapterError> {
1544 let (compute, location) = match config {
1546 mz_sql::plan::ReplicaConfig::Unorchestrated {
1547 storagectl_addrs,
1548 computectl_addrs,
1549 compute,
1550 } => {
1551 let location = mz_catalog::durable::ReplicaLocation::Unmanaged {
1552 storagectl_addrs,
1553 computectl_addrs,
1554 };
1555 (compute, location)
1556 }
1557 mz_sql::plan::ReplicaConfig::Orchestrated {
1558 availability_zone,
1559 billed_as,
1560 compute,
1561 internal,
1562 size,
1563 } => {
1564 let availability_zone = match availability_zone {
1565 Some(az) => {
1566 self.ensure_valid_azs([&az])?;
1567 Some(az)
1568 }
1569 None => None,
1570 };
1571 let location = mz_catalog::durable::ReplicaLocation::Managed {
1572 availability_zones: availability_zone.into_iter().collect(),
1575 billed_as,
1576 internal,
1577 size,
1578 pending: false,
1579 };
1580 (compute, location)
1581 }
1582 };
1583
1584 let logging = if let Some(config) = compute.introspection {
1585 ReplicaLogging {
1586 log_logging: config.debugging,
1587 interval: Some(config.interval),
1588 }
1589 } else {
1590 ReplicaLogging::default()
1591 };
1592
1593 let role_id = session.role_metadata().current_role;
1594 let config = ReplicaConfig {
1595 location: self.catalog().concretize_replica_location(
1596 location,
1597 &self
1598 .catalog()
1599 .get_role_allowed_cluster_sizes(&Some(role_id)),
1600 None,
1603 false,
1604 )?,
1605 compute: ComputeReplicaConfig { logging },
1606 };
1607
1608 let cluster = self.catalog().get_cluster(cluster_id);
1609
1610 if let ReplicaLocation::Managed(ManagedReplicaLocation {
1611 internal,
1612 billed_as,
1613 ..
1614 }) = &config.location
1615 {
1616 if !session.user().is_internal() && (*internal || billed_as.is_some()) {
1618 coord_bail!("cannot specify INTERNAL or BILLED AS as non-internal user")
1619 }
1620 if cluster.is_managed() && !*internal {
1622 coord_bail!("must specify INTERNAL when creating a replica in a managed cluster");
1623 }
1624 if billed_as.is_some() && !*internal {
1626 coord_bail!("must specify INTERNAL when specifying BILLED AS");
1627 }
1628 }
1629
1630 let owner_id = cluster.owner_id();
1633
1634 let cluster_name = cluster.name.clone();
1635 let is_builtin = cluster_id.is_system();
1636
1637 let id_ts = self.get_catalog_write_ts().await;
1645 let replica_id = self
1646 .catalog()
1647 .allocate_replica_ids(cluster_id, 1, id_ts)
1648 .await?
1649 .into_element();
1650
1651 let replica_ctx = match &config.location {
1654 ReplicaLocation::Managed(location) => Some(ReplicaEvalContext {
1655 cluster_id,
1656 replica_id,
1657 cluster: ClusterScopeContext {
1658 id: cluster_id.to_string(),
1659 name: cluster_name.clone(),
1660 is_builtin,
1661 },
1662 replica: ReplicaScopeContext {
1663 id: replica_id.to_string(),
1664 name: name.to_string(),
1665 is_builtin,
1666 size: location.size.clone(),
1667 size_family: location.allocation.family().to_string(),
1668 cluster_id: cluster_id.to_string(),
1669 cluster_name,
1670 },
1671 }),
1672 ReplicaLocation::Unmanaged(_) => None,
1673 };
1674
1675 let mut ops = vec![catalog::Op::CreateClusterReplica {
1676 cluster_id,
1677 replica_id,
1678 name: name.clone(),
1679 config,
1680 owner_id,
1681 reason: ReplicaCreateDropReason::Manual,
1682 }];
1683
1684 if let Some(replica_ctx) = replica_ctx {
1688 if let Some(scoped_op) = self.scoped_overrides_create_op(&[], &[replica_ctx]) {
1689 ops.push(scoped_op);
1690 }
1691 }
1692
1693 self.catalog_transact(Some(session), ops).await?;
1694
1695 Ok(ExecuteResponse::CreatedClusterReplica)
1696 }
1697
1698 pub(crate) async fn sequence_alter_cluster_managed_to_managed(
1707 &mut self,
1708 session: Option<&Session>,
1709 cluster_id: ClusterId,
1710 new_config: ClusterConfig,
1711 reason: ReplicaCreateDropReason,
1712 strategy: AlterClusterPlanStrategy,
1713 ) -> Result<NeedsFinalization, AdapterError> {
1714 let cluster = self.catalog.get_cluster(cluster_id);
1715 let name = cluster.name().to_string();
1716 let owner_id = cluster.owner_id();
1717
1718 let mut ops = vec![];
1719 let mut finalization_needed = NeedsFinalization::No;
1720
1721 let ClusterVariant::Managed(ClusterVariantManaged {
1722 size,
1723 availability_zones,
1724 logging,
1725 replication_factor,
1726 optimizer_feature_overrides: _,
1727 schedule: _,
1728 auto_scaling_strategy: _,
1729 reconfiguration: _,
1730 burst: _,
1731 }) = &cluster.config.variant
1732 else {
1733 panic!("expected existing managed cluster config");
1734 };
1735 let size = size.clone();
1739 let availability_zones = availability_zones.clone();
1740 let logging = logging.clone();
1741 let replication_factor = *replication_factor;
1742 let ClusterVariant::Managed(new_managed) = &new_config.variant else {
1743 panic!("expected new managed cluster config");
1744 };
1745 let ClusterVariantManaged {
1746 size: new_size,
1747 replication_factor: new_replication_factor,
1748 availability_zones: new_availability_zones,
1749 logging: new_logging,
1750 optimizer_feature_overrides: _,
1751 schedule: _,
1752 auto_scaling_strategy: _,
1753 reconfiguration: _,
1754 burst: _,
1755 } = new_managed;
1756
1757 let role_id = session.map(|s| s.role_metadata().current_role);
1758 self.catalog.ensure_valid_replica_size(
1759 &self.catalog().get_role_allowed_cluster_sizes(&role_id),
1760 new_size,
1761 false,
1762 )?;
1763
1764 if cluster.replicas().any(|r| r.config.location.pending()) {
1766 return Err(AlterClusterWhilePendingReplicas);
1767 }
1768
1769 let replica_id_by_name: BTreeMap<String, ReplicaId> = cluster
1772 .replicas()
1773 .map(|r| (r.name.clone(), r.replica_id))
1774 .collect();
1775
1776 let compute = mz_sql::plan::ComputeReplicaConfig {
1777 introspection: new_logging
1778 .interval
1779 .map(|interval| ComputeReplicaIntrospectionConfig {
1780 debugging: new_logging.log_logging,
1781 interval,
1782 }),
1783 };
1784
1785 if *new_replication_factor > replication_factor {
1790 if cluster_id.is_user() {
1791 self.validate_resource_limit(
1792 usize::cast_from(replication_factor),
1793 i64::from(*new_replication_factor) - i64::from(replication_factor),
1794 SystemVars::max_replicas_per_cluster,
1795 "cluster replica",
1796 MAX_REPLICAS_PER_CLUSTER.name(),
1797 )?;
1798 }
1799 }
1800
1801 let controller_owns = ENABLE_CLUSTER_CONTROLLER
1814 .get(self.catalog().system_config().dyncfgs())
1815 && cluster_id.is_user();
1816
1817 let config_changed = new_managed.replica_config_shape()
1824 != ManagedReplicaConfigShape::new(&size, &availability_zones, &logging);
1825 let needed_replica_ids = if controller_owns {
1826 0
1827 } else if config_changed {
1828 *new_replication_factor
1829 } else if *new_replication_factor > replication_factor {
1830 *new_replication_factor - replication_factor
1831 } else {
1832 0
1833 };
1834 let mut new_replica_ids = if needed_replica_ids > 0 {
1844 let id_ts = self.get_catalog_write_ts().await;
1845 self.catalog()
1846 .allocate_replica_ids(cluster_id, u64::from(needed_replica_ids), id_ts)
1847 .await?
1848 .into_iter()
1849 } else {
1850 Vec::<ReplicaId>::new().into_iter()
1851 };
1852
1853 let cluster_ctx = ClusterScopeContext {
1861 id: cluster_id.to_string(),
1862 name: name.clone(),
1863 is_builtin: cluster_id.is_system(),
1864 };
1865 let mut replica_ctxs = Vec::new();
1866
1867 if controller_owns {
1868 } else if config_changed {
1871 self.ensure_valid_azs(new_availability_zones.iter())?;
1872 match strategy {
1876 AlterClusterPlanStrategy::None => {
1877 let replica_ids_and_reasons = (0..replication_factor)
1878 .map(managed_cluster_replica_name)
1879 .filter_map(|name| replica_id_by_name.get(&name).copied())
1880 .map(|replica_id| {
1881 catalog::DropObjectInfo::ClusterReplica((
1882 cluster_id,
1883 replica_id,
1884 reason.clone(),
1885 ))
1886 })
1887 .collect();
1888 ops.push(catalog::Op::DropObjects(replica_ids_and_reasons));
1889 for replica_name in
1890 (0..*new_replication_factor).map(managed_cluster_replica_name)
1891 {
1892 let replica_id = new_replica_ids
1895 .next()
1896 .expect("pre-allocated enough replica ids");
1897 let size_family = self.create_managed_cluster_replica_op(
1898 cluster_id,
1899 replica_id,
1900 replica_name.clone(),
1901 &compute,
1902 new_size,
1903 &mut ops,
1904 Some(new_availability_zones.as_ref()),
1905 false,
1906 owner_id,
1907 reason.clone(),
1908 )?;
1909 replica_ctxs.push(ReplicaEvalContext {
1910 cluster_id,
1911 replica_id,
1912 cluster: cluster_ctx.clone(),
1913 replica: ReplicaScopeContext {
1914 id: replica_id.to_string(),
1915 name: replica_name,
1916 is_builtin: cluster_id.is_system(),
1917 size: new_size.clone(),
1918 size_family,
1919 cluster_id: cluster_id.to_string(),
1920 cluster_name: cluster_ctx.name.clone(),
1921 },
1922 });
1923 }
1924 }
1925 AlterClusterPlanStrategy::For(_) | AlterClusterPlanStrategy::UntilReady { .. } => {
1926 for replica_name in
1927 (0..*new_replication_factor).map(managed_cluster_replica_name)
1928 {
1929 let replica_name = format!("{replica_name}{PENDING_REPLICA_SUFFIX}");
1930 let replica_id = new_replica_ids
1931 .next()
1932 .expect("pre-allocated enough replica ids");
1933 let size_family = self.create_managed_cluster_replica_op(
1934 cluster_id,
1935 replica_id,
1936 replica_name.clone(),
1937 &compute,
1938 new_size,
1939 &mut ops,
1940 Some(new_availability_zones.as_ref()),
1941 true,
1942 owner_id,
1943 reason.clone(),
1944 )?;
1945 replica_ctxs.push(ReplicaEvalContext {
1946 cluster_id,
1947 replica_id,
1948 cluster: cluster_ctx.clone(),
1949 replica: ReplicaScopeContext {
1950 id: replica_id.to_string(),
1951 name: replica_name,
1952 is_builtin: cluster_id.is_system(),
1953 size: new_size.clone(),
1954 size_family,
1955 cluster_id: cluster_id.to_string(),
1956 cluster_name: cluster_ctx.name.clone(),
1957 },
1958 });
1959 }
1960 finalization_needed = NeedsFinalization::Yes;
1961 }
1962 }
1963 } else if *new_replication_factor < replication_factor {
1964 let replica_ids = (*new_replication_factor..replication_factor)
1966 .map(managed_cluster_replica_name)
1967 .filter_map(|name| replica_id_by_name.get(&name).copied())
1968 .map(|replica_id| {
1969 catalog::DropObjectInfo::ClusterReplica((
1970 cluster_id,
1971 replica_id,
1972 reason.clone(),
1973 ))
1974 })
1975 .collect();
1976 ops.push(catalog::Op::DropObjects(replica_ids));
1977 } else if *new_replication_factor > replication_factor {
1978 for replica_name in
1980 (replication_factor..*new_replication_factor).map(managed_cluster_replica_name)
1981 {
1982 let replica_id = new_replica_ids
1983 .next()
1984 .expect("pre-allocated enough replica ids");
1985 let size_family = self.create_managed_cluster_replica_op(
1986 cluster_id,
1987 replica_id,
1988 replica_name.clone(),
1989 &compute,
1990 new_size,
1991 &mut ops,
1992 Some(new_availability_zones.as_ref()),
1995 false,
1996 owner_id,
1997 reason.clone(),
1998 )?;
1999 replica_ctxs.push(ReplicaEvalContext {
2000 cluster_id,
2001 replica_id,
2002 cluster: cluster_ctx.clone(),
2003 replica: ReplicaScopeContext {
2004 id: replica_id.to_string(),
2005 name: replica_name,
2006 is_builtin: cluster_id.is_system(),
2007 size: new_size.clone(),
2008 size_family,
2009 cluster_id: cluster_id.to_string(),
2010 cluster_name: cluster_ctx.name.clone(),
2011 },
2012 });
2013 }
2014 }
2015
2016 match finalization_needed {
2028 NeedsFinalization::No => {
2029 let mut new_config = new_config;
2030 let reconfiguration_audit = if controller_owns {
2031 None
2032 } else {
2033 cancel_carried_reconfiguration(&mut new_config)
2034 };
2035 ops.push(catalog::Op::UpdateClusterConfig {
2036 id: cluster_id,
2037 name: name.clone(),
2038 config: new_config,
2039 reconfiguration_audit,
2040 burst_audit: None,
2041 });
2042 }
2043 NeedsFinalization::Yes => {}
2044 }
2045
2046 if let Some(scoped_op) = self.scoped_overrides_create_op(&[], &replica_ctxs) {
2053 ops.push(scoped_op);
2054 }
2055
2056 self.catalog_transact(session, ops).await?;
2057 Ok(finalization_needed)
2058 }
2059
2060 async fn sequence_alter_cluster_unmanaged_to_managed(
2064 &mut self,
2065 session: &Session,
2066 cluster_id: ClusterId,
2067 mut new_config: ClusterConfig,
2068 options: PlanClusterOption,
2069 ) -> Result<(), AdapterError> {
2070 let cluster = self.catalog.get_cluster(cluster_id);
2071 let cluster_name = cluster.name().to_string();
2072
2073 let ClusterVariant::Managed(ClusterVariantManaged {
2074 size: new_size,
2075 replication_factor: new_replication_factor,
2076 availability_zones: new_availability_zones,
2077 logging: _,
2078 optimizer_feature_overrides: _,
2079 schedule: _,
2080 auto_scaling_strategy: _,
2081 reconfiguration: _,
2082 burst: _,
2083 }) = &mut new_config.variant
2084 else {
2085 panic!("expected new managed cluster config");
2086 };
2087
2088 let user_replica_count = cluster
2090 .user_replicas()
2091 .count()
2092 .try_into()
2093 .expect("must_fit");
2094 match options.replication_factor {
2095 AlterOptionParameter::Set(_) => {
2096 if user_replica_count != *new_replication_factor {
2098 coord_bail!(
2099 "REPLICATION FACTOR {new_replication_factor} does not match number of replicas ({user_replica_count})"
2100 );
2101 }
2102 }
2103 _ => {
2104 *new_replication_factor = user_replica_count;
2105 }
2106 }
2107
2108 let mut names = BTreeSet::new();
2109 let mut sizes = BTreeSet::new();
2110
2111 self.ensure_valid_azs(new_availability_zones.iter())?;
2112
2113 for replica in cluster.user_replicas() {
2115 names.insert(replica.name.clone());
2116 match &replica.config.location {
2117 ReplicaLocation::Unmanaged(_) => coord_bail!(
2118 "Cannot convert unmanaged cluster with unmanaged replicas to managed cluster"
2119 ),
2120 ReplicaLocation::Managed(location) => {
2121 sizes.insert(location.size.clone());
2122
2123 for az in &location.availability_zones {
2127 if !new_availability_zones.contains(az) {
2128 coord_bail!(
2129 "unmanaged replica has availability zone {az} which is not \
2130 in managed {new_availability_zones:?}"
2131 )
2132 }
2133 }
2134 }
2135 }
2136 }
2137
2138 if sizes.is_empty() {
2139 assert!(
2140 cluster.user_replicas().next().is_none(),
2141 "Cluster should not have replicas"
2142 );
2143 match &options.size {
2145 AlterOptionParameter::Reset | AlterOptionParameter::Unchanged => {
2146 coord_bail!("Missing SIZE for empty cluster")
2147 }
2148 AlterOptionParameter::Set(_) => {} }
2150 } else if sizes.len() == 1 {
2151 let size = sizes.into_iter().next().expect("must exist");
2152 match &options.size {
2153 AlterOptionParameter::Set(sz) if *sz != size => {
2154 coord_bail!("Cluster replicas of size {size} do not match expected SIZE {sz}");
2155 }
2156 _ => *new_size = size,
2157 }
2158 } else {
2159 let formatted = sizes
2160 .iter()
2161 .map(String::as_str)
2162 .collect::<Vec<_>>()
2163 .join(", ");
2164 coord_bail!(
2165 "Cannot convert unmanaged cluster to managed, non-unique replica sizes: {formatted}"
2166 );
2167 }
2168
2169 for i in 0..*new_replication_factor {
2170 let name = managed_cluster_replica_name(i);
2171 names.remove(&name);
2172 }
2173 if !names.is_empty() {
2174 let formatted = names
2175 .iter()
2176 .map(String::as_str)
2177 .collect::<Vec<_>>()
2178 .join(", ");
2179 coord_bail!(
2180 "Cannot convert unmanaged cluster to managed, invalid replica names: {formatted}"
2181 );
2182 }
2183
2184 let ops = vec![catalog::Op::UpdateClusterConfig {
2185 id: cluster_id,
2186 name: cluster_name,
2187 config: new_config,
2188 reconfiguration_audit: None,
2189 burst_audit: None,
2190 }];
2191
2192 self.catalog_transact(Some(session), ops).await?;
2193 Ok(())
2194 }
2195
2196 async fn sequence_alter_cluster_managed_to_unmanaged(
2197 &mut self,
2198 session: &Session,
2199 cluster_id: ClusterId,
2200 new_config: ClusterConfig,
2201 ) -> Result<(), AdapterError> {
2202 let cluster = self.catalog().get_cluster(cluster_id);
2203
2204 if let ClusterVariant::Managed(managed) = &cluster.config.variant {
2210 if managed
2211 .reconfiguration
2212 .as_ref()
2213 .is_some_and(|record| record.is_in_progress())
2214 {
2215 return Err(AdapterError::AlterClusterUnmanagedWhileReconfiguring);
2216 }
2217 }
2218
2219 let ops = vec![catalog::Op::UpdateClusterConfig {
2220 id: cluster_id,
2221 name: cluster.name().to_string(),
2222 config: new_config,
2223 reconfiguration_audit: None,
2224 burst_audit: None,
2225 }];
2226
2227 self.catalog_transact(Some(session), ops).await?;
2228 Ok(())
2229 }
2230
2231 async fn sequence_alter_cluster_unmanaged_to_unmanaged(
2232 &mut self,
2233 session: &Session,
2234 cluster_id: ClusterId,
2235 new_config: ClusterConfig,
2236 replicas: AlterOptionParameter<Vec<(String, mz_sql::plan::ReplicaConfig)>>,
2237 ) -> Result<(), AdapterError> {
2238 if !matches!(replicas, AlterOptionParameter::Unchanged) {
2239 coord_bail!("Cannot alter replicas in unmanaged cluster");
2240 }
2241
2242 let cluster = self.catalog().get_cluster(cluster_id);
2243
2244 let ops = vec![catalog::Op::UpdateClusterConfig {
2245 id: cluster_id,
2246 name: cluster.name().to_string(),
2247 config: new_config,
2248 reconfiguration_audit: None,
2249 burst_audit: None,
2250 }];
2251
2252 self.catalog_transact(Some(session), ops).await?;
2253 Ok(())
2254 }
2255
2256 pub(crate) async fn sequence_alter_cluster_rename(
2257 &mut self,
2258 ctx: &mut ExecuteContext,
2259 AlterClusterRenamePlan { id, name, to_name }: AlterClusterRenamePlan,
2260 ) -> Result<ExecuteResponse, AdapterError> {
2261 let op = Op::RenameCluster {
2262 id,
2263 name,
2264 to_name,
2265 check_reserved_names: true,
2266 };
2267 match self
2268 .catalog_transact_with_ddl_transaction(ctx, vec![op], |_, _| Box::pin(async {}))
2269 .await
2270 {
2271 Ok(()) => Ok(ExecuteResponse::AlteredObject(ObjectType::Cluster)),
2272 Err(err) => Err(err),
2273 }
2274 }
2275
2276 pub(crate) async fn sequence_alter_cluster_swap(
2277 &mut self,
2278 ctx: &mut ExecuteContext,
2279 AlterClusterSwapPlan {
2280 id_a,
2281 id_b,
2282 name_a,
2283 name_b,
2284 name_temp,
2285 }: AlterClusterSwapPlan,
2286 ) -> Result<ExecuteResponse, AdapterError> {
2287 let op_a = Op::RenameCluster {
2288 id: id_a,
2289 name: name_a.clone(),
2290 to_name: name_temp.clone(),
2291 check_reserved_names: false,
2292 };
2293 let op_b = Op::RenameCluster {
2294 id: id_b,
2295 name: name_b.clone(),
2296 to_name: name_a,
2297 check_reserved_names: false,
2298 };
2299 let op_temp = Op::RenameCluster {
2300 id: id_a,
2301 name: name_temp,
2302 to_name: name_b,
2303 check_reserved_names: false,
2304 };
2305
2306 match self
2307 .catalog_transact_with_ddl_transaction(ctx, vec![op_a, op_b, op_temp], |_, _| {
2308 Box::pin(async {})
2309 })
2310 .await
2311 {
2312 Ok(()) => Ok(ExecuteResponse::AlteredObject(ObjectType::Cluster)),
2313 Err(err) => Err(err),
2314 }
2315 }
2316
2317 pub(crate) async fn sequence_alter_cluster_replica_rename(
2318 &mut self,
2319 session: &Session,
2320 AlterClusterReplicaRenamePlan {
2321 cluster_id,
2322 replica_id,
2323 name,
2324 to_name,
2325 }: AlterClusterReplicaRenamePlan,
2326 ) -> Result<ExecuteResponse, AdapterError> {
2327 let op = catalog::Op::RenameClusterReplica {
2328 cluster_id,
2329 replica_id,
2330 name,
2331 to_name,
2332 };
2333 match self.catalog_transact(Some(session), vec![op]).await {
2334 Ok(()) => Ok(ExecuteResponse::AlteredObject(ObjectType::ClusterReplica)),
2335 Err(err) => Err(err),
2336 }
2337 }
2338
2339 pub(crate) async fn sequence_alter_set_cluster(
2341 &self,
2342 _session: &Session,
2343 AlterSetClusterPlan { id, set_cluster: _ }: AlterSetClusterPlan,
2344 ) -> Result<ExecuteResponse, AdapterError> {
2345 async {}.await;
2349 let entry = self.catalog().get_entry(&id);
2350 match entry.item().typ() {
2351 _ => {
2352 Err(AdapterError::Unsupported("ALTER SET CLUSTER"))
2354 }
2355 }
2356 }
2357}
2358
2359fn managed_cluster_replica_name(index: u32) -> String {
2360 format!("r{}", index + 1)
2361}
2362
2363struct ReconfigurationDimensionsUnchanged {
2368 size: bool,
2369 replication_factor: bool,
2370 availability_zones: bool,
2371 log_logging: bool,
2372 interval: bool,
2373}
2374
2375fn cancel_carried_reconfiguration(config: &mut ClusterConfig) -> Option<ReconfigurationAudit> {
2386 let ClusterVariant::Managed(managed) = &mut config.variant else {
2387 return None;
2388 };
2389 let record = managed.reconfiguration.as_mut()?;
2390 if !record.is_in_progress() {
2391 return None;
2392 }
2393 record.status = ReconfigurationStatus::Cancelled;
2394 Some(ReconfigurationAudit::Cancelled)
2395}
2396
2397fn alter_changes_replica_shape(options: &PlanClusterOption) -> bool {
2407 use mz_sql::plan::AlterOptionParameter::Unchanged;
2408 let PlanClusterOption {
2409 availability_zones,
2410 introspection_debugging,
2411 introspection_interval,
2412 managed: _,
2413 replicas: _,
2414 replication_factor: _,
2415 size,
2416 schedule: _,
2417 workload_class: _,
2418 } = options;
2419 !matches!(size, Unchanged)
2420 || !matches!(availability_zones, Unchanged)
2421 || !matches!(introspection_debugging, Unchanged)
2422 || !matches!(introspection_interval, Unchanged)
2423}
2424
2425fn fold_reconfiguration_target(
2443 in_flight: Option<&ReconfigurationTarget>,
2444 new_target: ReconfigurationTarget,
2445 unchanged: ReconfigurationDimensionsUnchanged,
2446) -> ReconfigurationTarget {
2447 let Some(prev) = in_flight else {
2448 return new_target;
2449 };
2450 ReconfigurationTarget {
2451 size: if unchanged.size {
2452 prev.size.clone()
2453 } else {
2454 new_target.size
2455 },
2456 replication_factor: if unchanged.replication_factor {
2457 prev.replication_factor
2458 } else {
2459 new_target.replication_factor
2460 },
2461 availability_zones: if unchanged.availability_zones {
2462 prev.availability_zones.clone()
2463 } else {
2464 new_target.availability_zones
2465 },
2466 logging: ReplicaLogging {
2467 log_logging: if unchanged.log_logging {
2468 prev.logging.log_logging
2469 } else {
2470 new_target.logging.log_logging
2471 },
2472 interval: if unchanged.interval {
2473 prev.logging.interval
2474 } else {
2475 new_target.logging.interval
2476 },
2477 },
2478 }
2479}
2480
2481#[derive(PartialEq)]
2484pub(crate) enum NeedsFinalization {
2485 Yes,
2487 No,
2488}
2489
2490#[cfg(test)]
2491mod tests {
2492 use mz_controller::clusters::ReplicaLogging;
2493 use mz_controller_types::DEFAULT_REPLICA_LOGGING_INTERVAL;
2494
2495 use super::*;
2496
2497 fn target(size: &str, rf: u32, azs: &[&str], log_logging: bool) -> ReconfigurationTarget {
2498 ReconfigurationTarget {
2499 size: size.to_string(),
2500 replication_factor: rf,
2501 availability_zones: azs.iter().map(|s| s.to_string()).collect(),
2502 logging: ReplicaLogging {
2503 log_logging,
2504 interval: Some(DEFAULT_REPLICA_LOGGING_INTERVAL),
2505 },
2506 }
2507 }
2508
2509 fn all_changed() -> ReconfigurationDimensionsUnchanged {
2510 ReconfigurationDimensionsUnchanged {
2511 size: false,
2512 replication_factor: false,
2513 availability_zones: false,
2514 log_logging: false,
2515 interval: false,
2516 }
2517 }
2518
2519 fn all_unchanged() -> ReconfigurationDimensionsUnchanged {
2520 ReconfigurationDimensionsUnchanged {
2521 size: true,
2522 replication_factor: true,
2523 availability_zones: true,
2524 log_logging: true,
2525 interval: true,
2526 }
2527 }
2528
2529 #[mz_ore::test]
2530 fn fold_with_no_record_takes_new_target() {
2531 let new = target("200cc", 3, &["az1"], true);
2533 let folded = fold_reconfiguration_target(None, new.clone(), all_changed());
2534 assert_eq!(folded, new);
2535 }
2536
2537 #[mz_ore::test]
2538 fn fold_rf_only_keeps_in_flight_shape() {
2539 let in_flight = target("200cc", 1, &["az2"], true);
2543 let new = target("100cc", 5, &["az1"], false);
2546 let unchanged = ReconfigurationDimensionsUnchanged {
2547 size: true,
2548 replication_factor: false,
2549 availability_zones: true,
2550 log_logging: true,
2551 interval: true,
2552 };
2553 let folded = fold_reconfiguration_target(Some(&in_flight), new, unchanged);
2554 assert_eq!(folded, target("200cc", 5, &["az2"], true));
2556 }
2557
2558 #[mz_ore::test]
2559 fn fold_with_all_set_overwrites_every_dimension() {
2560 let in_flight = target("200cc", 1, &["az2"], true);
2562 let new = target("400cc", 9, &["az9"], false);
2563 let folded = fold_reconfiguration_target(Some(&in_flight), new.clone(), all_changed());
2564 assert_eq!(folded, new);
2565 }
2566
2567 #[mz_ore::test]
2568 fn fold_all_unchanged_is_alter_back_to_in_flight() {
2569 let in_flight = target("200cc", 2, &["az2"], true);
2574 let realized_shaped = target("100cc", 1, &["az1"], false);
2575 let folded =
2576 fold_reconfiguration_target(Some(&in_flight), realized_shaped, all_unchanged());
2577 assert_eq!(folded, in_flight);
2578 }
2579
2580 #[mz_ore::test]
2581 fn fold_logging_subdimensions_fold_independently() {
2582 let mut in_flight = target("100cc", 1, &["az1"], false);
2587 in_flight.logging.interval = Some(Duration::from_secs(5));
2588 let new = target("100cc", 1, &["az1"], true);
2589 let unchanged = ReconfigurationDimensionsUnchanged {
2590 size: true,
2591 replication_factor: true,
2592 availability_zones: true,
2593 log_logging: false,
2594 interval: true,
2595 };
2596 let folded = fold_reconfiguration_target(Some(&in_flight), new, unchanged);
2597 assert_eq!(
2598 folded.logging,
2599 ReplicaLogging {
2600 log_logging: true,
2601 interval: Some(Duration::from_secs(5)),
2602 }
2603 );
2604 }
2605}