1use std::collections::{BTreeMap, BTreeSet};
13use std::fmt;
14use std::num::NonZero;
15use std::str::FromStr;
16use std::sync::Arc;
17use std::sync::LazyLock;
18use std::time::Duration;
19
20use anyhow::anyhow;
21use bytesize::ByteSize;
22use chrono::{DateTime, Utc};
23use futures::stream::{BoxStream, StreamExt};
24use mz_cluster_client::client::{ClusterReplicaLocation, TimelyConfig};
25use mz_compute_client::logging::LogVariant;
26use mz_compute_types::config::{ComputeReplicaConfig, ComputeReplicaLogging};
27use mz_controller_types::dyncfgs::{
28 ARRANGEMENT_EXERT_PROPORTIONALITY, CONTROLLER_PAST_GENERATION_REPLICA_CLEANUP_RETRY_INTERVAL,
29 ENABLE_TIMELY_ZERO_COPY, ENABLE_TIMELY_ZERO_COPY_LGALLOC, TIMELY_ZERO_COPY_LIMIT,
30};
31use mz_controller_types::{ClusterId, ReplicaId};
32use mz_orchestrator::NamespacedOrchestrator;
33use mz_orchestrator::{
34 CpuLimit, DiskLimit, LabelSelectionLogic, LabelSelector, MemoryLimit, Service, ServiceConfig,
35 ServiceEvent, ServicePort,
36};
37use mz_ore::cast::CastInto;
38use mz_ore::task::{self, AbortOnDropHandle};
39use mz_ore::{halt, instrument};
40use mz_repr::GlobalId;
41use mz_repr::adt::numeric::Numeric;
42use regex::Regex;
43use serde::{Deserialize, Serialize};
44use tokio::time;
45use tracing::{error, info, warn};
46
47use crate::Controller;
48
49pub struct ClusterConfig {
51 pub arranged_logs: BTreeMap<LogVariant, GlobalId>,
56 pub workload_class: Option<String>,
59}
60
61pub type ClusterStatus = mz_orchestrator::ServiceStatus;
63
64#[derive(Clone, Debug, Serialize, PartialEq)]
66pub struct ReplicaConfig {
67 pub location: ReplicaLocation,
69 pub compute: ComputeReplicaConfig,
71}
72
73#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
75pub struct ReplicaAllocation {
76 pub memory_limit: Option<MemoryLimit>,
78 pub cpu_limit: Option<CpuLimit>,
80 pub cpu_request: Option<CpuLimit>,
82 pub disk_limit: Option<DiskLimit>,
84 pub scale: NonZero<u16>,
86 pub workers: NonZero<usize>,
88 #[serde(deserialize_with = "mz_repr::adt::numeric::str_serde::deserialize")]
90 pub credits_per_hour: Numeric,
91 #[serde(default)]
93 pub cpu_exclusive: bool,
94 #[serde(default = "default_true")]
97 pub is_cc: bool,
98 #[serde(default)]
107 pub family: Option<String>,
108 #[serde(default)]
110 pub swap_enabled: bool,
111 #[serde(default)]
113 pub disabled: bool,
114 #[serde(default)]
116 pub selectors: BTreeMap<String, String>,
117}
118
119impl ReplicaAllocation {
120 pub fn family(&self) -> &str {
129 match &self.family {
130 Some(family) => family.as_str(),
131 None if self.is_cc => "cc",
132 None => "legacy",
133 }
134 }
135}
136
137fn default_true() -> bool {
138 true
139}
140
141#[mz_ore::test]
142#[cfg_attr(miri, ignore)] fn test_replica_allocation_deserialization() {
145 use bytesize::ByteSize;
146 use mz_ore::{assert_err, assert_ok};
147
148 let data = r#"
149 {
150 "cpu_limit": 1.0,
151 "memory_limit": "10GiB",
152 "disk_limit": "100MiB",
153 "scale": 16,
154 "workers": 1,
155 "credits_per_hour": "16",
156 "swap_enabled": true,
157 "selectors": {
158 "key1": "value1",
159 "key2": "value2"
160 }
161 }"#;
162
163 let replica_allocation: ReplicaAllocation = serde_json::from_str(data)
164 .expect("deserialization from JSON succeeds for ReplicaAllocation");
165
166 assert_eq!(
167 replica_allocation,
168 ReplicaAllocation {
169 credits_per_hour: 16.into(),
170 disk_limit: Some(DiskLimit(ByteSize::mib(100))),
171 disabled: false,
172 memory_limit: Some(MemoryLimit(ByteSize::gib(10))),
173 cpu_limit: Some(CpuLimit::from_millicpus(1000)),
174 cpu_request: None,
175 cpu_exclusive: false,
176 is_cc: true,
177 family: None,
178 swap_enabled: true,
179 scale: NonZero::new(16).unwrap(),
180 workers: NonZero::new(1).unwrap(),
181 selectors: BTreeMap::from([
182 ("key1".to_string(), "value1".to_string()),
183 ("key2".to_string(), "value2".to_string())
184 ]),
185 }
186 );
187
188 let data = r#"
189 {
190 "cpu_limit": 0,
191 "memory_limit": "0GiB",
192 "disk_limit": "0MiB",
193 "scale": 1,
194 "workers": 1,
195 "credits_per_hour": "0",
196 "cpu_exclusive": true,
197 "disabled": true
198 }"#;
199
200 let replica_allocation: ReplicaAllocation = serde_json::from_str(data)
201 .expect("deserialization from JSON succeeds for ReplicaAllocation");
202
203 assert_eq!(
204 replica_allocation,
205 ReplicaAllocation {
206 credits_per_hour: 0.into(),
207 disk_limit: Some(DiskLimit(ByteSize::mib(0))),
208 disabled: true,
209 memory_limit: Some(MemoryLimit(ByteSize::gib(0))),
210 cpu_limit: Some(CpuLimit::from_millicpus(0)),
211 cpu_request: None,
212 cpu_exclusive: true,
213 is_cc: true,
214 family: None,
215 swap_enabled: false,
216 scale: NonZero::new(1).unwrap(),
217 workers: NonZero::new(1).unwrap(),
218 selectors: Default::default(),
219 }
220 );
221
222 let data = r#"{"scale": 0, "workers": 1, "credits_per_hour": "0"}"#;
224 assert_err!(serde_json::from_str::<ReplicaAllocation>(data));
225 let data = r#"{"scale": 1, "workers": 0, "credits_per_hour": "0"}"#;
226 assert_err!(serde_json::from_str::<ReplicaAllocation>(data));
227 let data = r#"{"scale": 1, "workers": 1, "credits_per_hour": "0"}"#;
228 assert_ok!(serde_json::from_str::<ReplicaAllocation>(data));
229}
230
231#[mz_ore::test]
232#[cfg_attr(miri, ignore)] fn test_replica_allocation_family() {
234 let parse = |json: &str| -> ReplicaAllocation {
235 serde_json::from_str(json).expect("deserialization from JSON succeeds")
236 };
237
238 assert_eq!(
240 parse(r#"{"scale": 1, "workers": 1, "credits_per_hour": "0", "family": "D"}"#).family(),
241 "D"
242 );
243 assert_eq!(
246 parse(r#"{"scale": 1, "workers": 1, "credits_per_hour": "0"}"#).family(),
247 "cc"
248 );
249 assert_eq!(
252 parse(r#"{"scale": 1, "workers": 1, "credits_per_hour": "0", "is_cc": false}"#).family(),
253 "legacy"
254 );
255 assert_eq!(
257 parse(
258 r#"{"scale": 1, "workers": 1, "credits_per_hour": "0", "is_cc": false, "family": "legacy-special"}"#
259 )
260 .family(),
261 "legacy-special"
262 );
263}
264
265#[derive(Clone, Debug, Serialize, PartialEq)]
267pub enum ReplicaLocation {
268 Unmanaged(UnmanagedReplicaLocation),
270 Managed(ManagedReplicaLocation),
272}
273
274impl ReplicaLocation {
275 pub fn num_processes(&self) -> usize {
277 match self {
278 ReplicaLocation::Unmanaged(UnmanagedReplicaLocation {
279 computectl_addrs, ..
280 }) => computectl_addrs.len(),
281 ReplicaLocation::Managed(ManagedReplicaLocation { allocation, .. }) => {
282 allocation.scale.cast_into()
283 }
284 }
285 }
286
287 pub fn billed_as(&self) -> Option<&str> {
288 match self {
289 ReplicaLocation::Managed(ManagedReplicaLocation { billed_as, .. }) => {
290 billed_as.as_deref()
291 }
292 ReplicaLocation::Unmanaged(_) => None,
293 }
294 }
295
296 pub fn internal(&self) -> bool {
297 match self {
298 ReplicaLocation::Managed(ManagedReplicaLocation { internal, .. }) => *internal,
299 ReplicaLocation::Unmanaged(_) => false,
300 }
301 }
302
303 pub fn workers(&self) -> Option<usize> {
307 match self {
308 ReplicaLocation::Managed(ManagedReplicaLocation { allocation, .. }) => {
309 Some(allocation.workers.get() * self.num_processes())
310 }
311 ReplicaLocation::Unmanaged(_) => None,
312 }
313 }
314
315 pub fn pending(&self) -> bool {
321 match self {
322 ReplicaLocation::Managed(ManagedReplicaLocation { pending, .. }) => *pending,
323 ReplicaLocation::Unmanaged(_) => false,
324 }
325 }
326}
327
328#[derive(Debug, Clone)]
331pub enum ClusterRole {
332 SystemCritical,
335 System,
339 User,
342}
343
344#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
346pub struct UnmanagedReplicaLocation {
347 pub storagectl_addrs: Vec<String>,
350 pub computectl_addrs: Vec<String>,
353}
354
355#[derive(Clone, Debug, Serialize, PartialEq)]
357pub struct ManagedReplicaLocation {
358 pub allocation: ReplicaAllocation,
360 pub size: String,
362 pub internal: bool,
364 pub billed_as: Option<String>,
366 #[serde(skip)]
377 pub availability_zones: Vec<String>,
378 pub pending: bool,
380}
381
382impl ManagedReplicaLocation {
383 pub fn size_for_billing(&self) -> &str {
385 self.billed_as.as_deref().unwrap_or(&self.size)
386 }
387}
388
389pub type ReplicaLogging = ComputeReplicaLogging;
391
392pub type ProcessId = u64;
394
395#[derive(Debug, Clone, Serialize)]
397pub struct ClusterEvent {
398 pub cluster_id: ClusterId,
399 pub replica_id: ReplicaId,
400 pub process_id: ProcessId,
401 pub status: ClusterStatus,
402 pub restart_count: u64,
405 pub time: DateTime<Utc>,
406}
407
408impl Controller {
409 pub fn create_cluster(
415 &mut self,
416 id: ClusterId,
417 config: ClusterConfig,
418 ) -> Result<(), anyhow::Error> {
419 self.storage
420 .create_instance(id, config.workload_class.clone());
421 self.compute
422 .create_instance(id, config.arranged_logs, config.workload_class)?;
423 Ok(())
424 }
425
426 pub fn update_cluster_workload_class(&mut self, id: ClusterId, workload_class: Option<String>) {
432 self.storage
433 .update_instance_workload_class(id, workload_class.clone());
434 self.compute
435 .update_instance_workload_class(id, workload_class)
436 .expect("instance exists");
437 }
438
439 pub fn drop_cluster(&mut self, id: ClusterId) {
445 self.storage.drop_instance(id);
446 self.compute.drop_instance(id);
447 }
448
449 pub fn create_replica(
452 &mut self,
453 cluster_id: ClusterId,
454 replica_id: ReplicaId,
455 cluster_name: String,
456 replica_name: String,
457 role: ClusterRole,
458 config: ReplicaConfig,
459 enable_worker_core_affinity: bool,
460 enable_storage_introspection_logs: bool,
461 ) -> Result<(), anyhow::Error> {
462 let storage_location: ClusterReplicaLocation;
463 let compute_location: ClusterReplicaLocation;
464 let metrics_task: Option<AbortOnDropHandle<()>>;
465
466 match config.location {
467 ReplicaLocation::Unmanaged(UnmanagedReplicaLocation {
468 storagectl_addrs,
469 computectl_addrs,
470 }) => {
471 compute_location = ClusterReplicaLocation {
472 ctl_addrs: computectl_addrs,
473 };
474 storage_location = ClusterReplicaLocation {
475 ctl_addrs: storagectl_addrs,
476 };
477 metrics_task = None;
478 }
479 ReplicaLocation::Managed(m) => {
480 let (service, metrics_task_join_handle) = self.provision_replica(
481 cluster_id,
482 replica_id,
483 cluster_name,
484 replica_name,
485 role,
486 m,
487 enable_worker_core_affinity,
488 enable_storage_introspection_logs,
489 )?;
490 storage_location = ClusterReplicaLocation {
491 ctl_addrs: service.addresses("storagectl"),
492 };
493 compute_location = ClusterReplicaLocation {
494 ctl_addrs: service.addresses("computectl"),
495 };
496 metrics_task = Some(metrics_task_join_handle);
497
498 let http_addresses = service.addresses("internal-http");
500 self.replica_http_locator
501 .register_replica(cluster_id, replica_id, http_addresses);
502 }
503 }
504
505 self.storage
506 .connect_replica(cluster_id, replica_id, storage_location);
507 self.compute.add_replica_to_instance(
508 cluster_id,
509 replica_id,
510 compute_location,
511 config.compute,
512 )?;
513
514 if let Some(task) = metrics_task {
515 self.metrics_tasks.insert(replica_id, task);
516 }
517
518 Ok(())
519 }
520
521 pub fn drop_replica(
523 &mut self,
524 cluster_id: ClusterId,
525 replica_id: ReplicaId,
526 ) -> Result<(), anyhow::Error> {
527 self.deprovision_replica(cluster_id, replica_id, self.deploy_generation)?;
532 self.metrics_tasks.remove(&replica_id);
533
534 self.replica_http_locator
536 .remove_replica(cluster_id, replica_id);
537
538 self.replica_dyncfg_overrides.remove(&replica_id);
542
543 self.compute.drop_replica(cluster_id, replica_id)?;
544 self.storage.drop_replica(cluster_id, replica_id);
545 Ok(())
546 }
547
548 pub(crate) fn remove_past_generation_replicas_in_background(&self) {
550 let deploy_generation = self.deploy_generation;
551 let dyncfg = Arc::clone(self.compute.dyncfg());
552 let orchestrator = Arc::clone(&self.orchestrator);
553 task::spawn(
554 || "controller_remove_past_generation_replicas",
555 async move {
556 info!("attempting to remove past generation replicas");
557 loop {
558 match try_remove_past_generation_replicas(&*orchestrator, deploy_generation)
559 .await
560 {
561 Ok(()) => {
562 info!("successfully removed past generation replicas");
563 return;
564 }
565 Err(e) => {
566 let interval =
567 CONTROLLER_PAST_GENERATION_REPLICA_CLEANUP_RETRY_INTERVAL
568 .get(&dyncfg);
569 warn!(%e, "failed to remove past generation replicas; will retry in {interval:?}");
570 time::sleep(interval).await;
571 }
572 }
573 }
574 },
575 );
576 }
577
578 #[instrument]
580 pub async fn remove_orphaned_replicas(
581 &mut self,
582 next_user_replica_id: u64,
583 next_system_replica_id: u64,
584 ) -> Result<(), anyhow::Error> {
585 let desired: BTreeSet<_> = self.metrics_tasks.keys().copied().collect();
586
587 let actual: BTreeSet<_> = self
588 .orchestrator
589 .list_services()
590 .await?
591 .iter()
592 .map(|s| ReplicaServiceName::from_str(s))
593 .collect::<Result<_, _>>()?;
594
595 for ReplicaServiceName {
596 cluster_id,
597 replica_id,
598 generation,
599 } in actual
600 {
601 if generation != self.deploy_generation {
605 continue;
606 }
607
608 let smaller_next = match replica_id {
609 ReplicaId::User(id) if id >= next_user_replica_id => {
610 Some(ReplicaId::User(next_user_replica_id))
611 }
612 ReplicaId::System(id) if id >= next_system_replica_id => {
613 Some(ReplicaId::System(next_system_replica_id))
614 }
615 _ => None,
616 };
617 if let Some(next) = smaller_next {
618 halt!("found replica ID ({replica_id}) in orchestrator >= next ID ({next})");
623 }
624 if !desired.contains(&replica_id) {
625 self.deprovision_replica(cluster_id, replica_id, generation)?;
626 }
627 }
628
629 Ok(())
630 }
631
632 pub fn events_stream(&self) -> BoxStream<'static, ClusterEvent> {
633 let deploy_generation = self.deploy_generation;
634
635 fn translate_event(event: ServiceEvent) -> Result<(ClusterEvent, u64), anyhow::Error> {
636 let ReplicaServiceName {
637 cluster_id,
638 replica_id,
639 generation: replica_generation,
640 ..
641 } = event.service_id.parse()?;
642
643 let event = ClusterEvent {
644 cluster_id,
645 replica_id,
646 process_id: event.process_id,
647 status: event.status,
648 restart_count: event.restart_count,
649 time: event.time,
650 };
651
652 Ok((event, replica_generation))
653 }
654
655 let stream = self
656 .orchestrator
657 .watch_services()
658 .map(|event| event.and_then(translate_event))
659 .filter_map(move |event| async move {
660 match event {
661 Ok((event, replica_generation)) => {
662 if replica_generation == deploy_generation {
663 Some(event)
664 } else {
665 None
666 }
667 }
668 Err(error) => {
669 error!("service watch error: {error}");
670 None
671 }
672 }
673 });
674
675 Box::pin(stream)
676 }
677
678 fn provision_replica(
680 &self,
681 cluster_id: ClusterId,
682 replica_id: ReplicaId,
683 cluster_name: String,
684 replica_name: String,
685 role: ClusterRole,
686 location: ManagedReplicaLocation,
687 enable_worker_core_affinity: bool,
688 enable_storage_introspection_logs: bool,
689 ) -> Result<(Box<dyn Service>, AbortOnDropHandle<()>), anyhow::Error> {
690 let service_name = ReplicaServiceName {
691 cluster_id,
692 replica_id,
693 generation: self.deploy_generation,
694 }
695 .to_string();
696 let role_label = match role {
697 ClusterRole::SystemCritical => "system-critical",
698 ClusterRole::System => "system",
699 ClusterRole::User => "user",
700 };
701 let environment_id = self.connection_context().environment_id.clone();
702 let aws_external_id_prefix = self.connection_context().aws_external_id_prefix.clone();
703 let aws_connection_role_arn = self.connection_context().aws_connection_role_arn.clone();
704 let persist_pubsub_url = self.persist_pubsub_url.clone();
705 let secrets_args = self.secrets_args.to_flags();
706
707 let storage_proto_timely_config = TimelyConfig {
709 arrangement_exert_proportionality: 1337,
710 ..Default::default()
711 };
712 let overrides = self.replica_dyncfg_overrides.get(&replica_id);
719 let compute_proto_timely_config = TimelyConfig {
720 arrangement_exert_proportionality: ARRANGEMENT_EXERT_PROPORTIONALITY
721 .get_with_overrides(&self.dyncfg, overrides),
722 enable_zero_copy: ENABLE_TIMELY_ZERO_COPY.get_with_overrides(&self.dyncfg, overrides),
723 enable_zero_copy_lgalloc: ENABLE_TIMELY_ZERO_COPY_LGALLOC
724 .get_with_overrides(&self.dyncfg, overrides),
725 zero_copy_limit: TIMELY_ZERO_COPY_LIMIT.get_with_overrides(&self.dyncfg, overrides),
726 ..Default::default()
727 };
728
729 let mut disk_limit = location.allocation.disk_limit;
730 let memory_limit = location.allocation.memory_limit;
731 let mut memory_request = None;
732
733 if location.allocation.swap_enabled {
734 disk_limit = Some(DiskLimit::ZERO);
738
739 memory_request = memory_limit.map(|MemoryLimit(limit)| {
743 let request = ByteSize::b(limit.as_u64() - 1);
744 MemoryLimit(request)
745 });
746 }
747
748 let service = self.orchestrator.ensure_service(
749 &service_name,
750 ServiceConfig {
751 app_name: "clusterd".into(),
752 image: self.clusterd_image.clone(),
753 init_container_image: self.init_container_image.clone(),
754 args: Box::new(move |assigned| {
755 let storage_timely_config = TimelyConfig {
756 workers: location.allocation.workers.get(),
757 addresses: assigned.peer_addresses("storage"),
758 ..storage_proto_timely_config
759 };
760 let compute_timely_config = TimelyConfig {
761 workers: location.allocation.workers.get(),
762 addresses: assigned.peer_addresses("compute"),
763 ..compute_proto_timely_config
764 };
765
766 let mut args = vec![
767 format!(
768 "--storage-controller-listen-addr={}",
769 assigned.listen_addrs["storagectl"]
770 ),
771 format!(
772 "--compute-controller-listen-addr={}",
773 assigned.listen_addrs["computectl"]
774 ),
775 format!(
776 "--internal-http-listen-addr={}",
777 assigned.listen_addrs["internal-http"]
778 ),
779 format!("--opentelemetry-resource=cluster_id={}", cluster_id),
780 format!("--opentelemetry-resource=replica_id={}", replica_id),
781 format!("--persist-pubsub-url={}", persist_pubsub_url),
782 format!("--environment-id={}", environment_id),
783 format!(
784 "--storage-timely-config={}",
785 storage_timely_config.to_string(),
786 ),
787 format!(
788 "--compute-timely-config={}",
789 compute_timely_config.to_string(),
790 ),
791 ];
792 if let Some(aws_external_id_prefix) = &aws_external_id_prefix {
793 args.push(format!(
794 "--aws-external-id-prefix={}",
795 aws_external_id_prefix
796 ));
797 }
798 if let Some(aws_connection_role_arn) = &aws_connection_role_arn {
799 args.push(format!(
800 "--aws-connection-role-arn={}",
801 aws_connection_role_arn
802 ));
803 }
804 if let Some(memory_limit) = location.allocation.memory_limit {
805 args.push(format!(
806 "--announce-memory-limit={}",
807 memory_limit.0.as_u64()
808 ));
809 }
810 if location.allocation.cpu_exclusive && enable_worker_core_affinity {
811 args.push("--worker-core-affinity".into());
812 }
813 if enable_storage_introspection_logs {
814 args.push("--enable-storage-introspection-logs".into());
815 }
816 if location.allocation.is_cc {
817 args.push("--is-cc".into());
818 }
819
820 if location.allocation.swap_enabled
823 && let Some(memory_limit) = location.allocation.memory_limit
824 && let Some(disk_limit) = location.allocation.disk_limit
825 && disk_limit != DiskLimit::ZERO
829 {
830 let heap_limit = memory_limit.0 + disk_limit.0;
831 args.push(format!("--heap-limit={}", heap_limit.as_u64()));
832 }
833
834 args.extend(secrets_args.clone());
835 args
836 }),
837 ports: vec![
838 ServicePort {
839 name: "storagectl".into(),
840 port_hint: 2100,
841 },
842 ServicePort {
846 name: "storage".into(),
847 port_hint: 2103,
848 },
849 ServicePort {
850 name: "computectl".into(),
851 port_hint: 2101,
852 },
853 ServicePort {
854 name: "compute".into(),
855 port_hint: 2102,
856 },
857 ServicePort {
858 name: "internal-http".into(),
859 port_hint: 6878,
860 },
861 ],
862 cpu_limit: location.allocation.cpu_limit,
863 cpu_request: location.allocation.cpu_request,
864 memory_limit,
865 memory_request,
866 scale: location.allocation.scale,
867 labels: BTreeMap::from([
868 ("replica-id".into(), replica_id.to_string()),
869 ("cluster-id".into(), cluster_id.to_string()),
870 ("generation".into(), self.deploy_generation.to_string()),
871 ("type".into(), "cluster".into()),
872 ("replica-role".into(), role_label.into()),
873 ("workers".into(), location.allocation.workers.to_string()),
874 (
875 "size".into(),
876 location
877 .size
878 .to_string()
879 .replace("=", "-")
880 .replace(",", "_"),
881 ),
882 ]),
883 annotations: BTreeMap::from([
884 (
885 "replica-name".into(),
886 format!("{cluster_name}.{replica_name}"),
887 ),
888 ("cluster-name".into(), cluster_name),
889 ]),
890 availability_zones: Some(location.availability_zones).filter(|azs| !azs.is_empty()),
893 other_replicas_selector: vec![
906 LabelSelector {
907 label_name: "cluster-id".to_string(),
908 logic: LabelSelectionLogic::Eq {
909 value: cluster_id.to_string(),
910 },
911 },
912 LabelSelector {
914 label_name: "replica-id".into(),
915 logic: LabelSelectionLogic::NotEq {
916 value: replica_id.to_string(),
917 },
918 },
919 LabelSelector {
920 label_name: "generation".into(),
921 logic: LabelSelectionLogic::Eq {
922 value: self.deploy_generation.to_string(),
923 },
924 },
925 ],
926 replicas_selector: vec![
927 LabelSelector {
928 label_name: "cluster-id".to_string(),
929 logic: LabelSelectionLogic::Eq {
931 value: cluster_id.to_string(),
932 },
933 },
934 LabelSelector {
935 label_name: "generation".into(),
936 logic: LabelSelectionLogic::Eq {
937 value: self.deploy_generation.to_string(),
938 },
939 },
940 ],
941 disk_limit,
942 node_selector: location.allocation.selectors,
943 },
944 )?;
945
946 let metrics_task = mz_ore::task::spawn(|| format!("replica-metrics-{replica_id}"), {
947 let tx = self.metrics_tx.clone();
948 let orchestrator = Arc::clone(&self.orchestrator);
949 let service_name = service_name.clone();
950 async move {
951 const METRICS_INTERVAL: Duration = Duration::from_secs(60);
952
953 let mut interval = tokio::time::interval(METRICS_INTERVAL);
961 loop {
962 interval.tick().await;
963 match orchestrator.fetch_service_metrics(&service_name).await {
964 Ok(metrics) => {
965 let _ = tx.send((replica_id, metrics));
966 }
967 Err(e) => {
968 warn!("failed to get metrics for replica {replica_id}: {e}");
969 }
970 }
971 }
972 }
973 });
974
975 Ok((service, metrics_task.abort_on_drop()))
976 }
977
978 fn deprovision_replica(
980 &self,
981 cluster_id: ClusterId,
982 replica_id: ReplicaId,
983 generation: u64,
984 ) -> Result<(), anyhow::Error> {
985 let service_name = ReplicaServiceName {
986 cluster_id,
987 replica_id,
988 generation,
989 }
990 .to_string();
991 self.orchestrator.drop_service(&service_name)
992 }
993}
994
995async fn try_remove_past_generation_replicas(
997 orchestrator: &dyn NamespacedOrchestrator,
998 deploy_generation: u64,
999) -> Result<(), anyhow::Error> {
1000 let services: BTreeSet<_> = orchestrator.list_services().await?.into_iter().collect();
1001
1002 for service in services {
1003 let name: ReplicaServiceName = service.parse()?;
1004 if name.generation < deploy_generation {
1005 info!(
1006 cluster_id = %name.cluster_id,
1007 replica_id = %name.replica_id,
1008 "removing past generation replica",
1009 );
1010 orchestrator.drop_service(&service)?;
1011 }
1012 }
1013
1014 Ok(())
1015}
1016
1017#[derive(PartialEq, Eq, PartialOrd, Ord)]
1019pub struct ReplicaServiceName {
1020 pub cluster_id: ClusterId,
1021 pub replica_id: ReplicaId,
1022 pub generation: u64,
1023}
1024
1025impl fmt::Display for ReplicaServiceName {
1026 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1027 let ReplicaServiceName {
1028 cluster_id,
1029 replica_id,
1030 generation,
1031 } = self;
1032 write!(f, "{cluster_id}-replica-{replica_id}-gen-{generation}")
1033 }
1034}
1035
1036impl FromStr for ReplicaServiceName {
1037 type Err = anyhow::Error;
1038
1039 fn from_str(s: &str) -> Result<Self, Self::Err> {
1040 static SERVICE_NAME_RE: LazyLock<Regex> = LazyLock::new(|| {
1041 Regex::new(r"(?-u)^([us]\d+)-replica-([us]\d+)(?:-gen-(\d+))?$").unwrap()
1042 });
1043
1044 let caps = SERVICE_NAME_RE
1045 .captures(s)
1046 .ok_or_else(|| anyhow!("invalid service name: {s}"))?;
1047
1048 Ok(ReplicaServiceName {
1049 cluster_id: caps.get(1).unwrap().as_str().parse().unwrap(),
1050 replica_id: caps.get(2).unwrap().as_str().parse().unwrap(),
1051 generation: caps.get(3).map_or("0", |m| m.as_str()).parse().unwrap(),
1055 })
1056 }
1057}