1use hydracache::{ClusterGridCounters, HydraCache};
2use hydracache_client_transport_axum::{ClientSurfaceDrain, ClientSurfaceRuntime};
3use hydracache_observability::{
4 ClusterMemberView, ClusterOverview, ClusterTopologyOverview, ConsistencyView,
5 HydraCacheRegistry, LeaderView, LifecycleView, PartitionSummary, TopologyReshardPhase,
6 TopologyStatusSource,
7};
8use hydracache_redis_compat::{RedisListenerConfig, RedisRespServer, RedisServeError};
9use serde::Serialize;
10use std::net::SocketAddr;
11use std::sync::Arc;
12use std::time::Instant;
13use thiserror::Error;
14
15use crate::cluster_status::{
16 ClusterStatus, ClusterStatusProvider, ClusterStatusRuntime, GridControlPlaneHandle,
17 LiveClusterStatus, MemberRole, ModeledClusterStatus, RaftCompactionError, RaftCompactionStatus,
18 Reachability, ReshardPhase, StatusSource,
19};
20use crate::config::{ServerConfig, ServerConfigError, ServerRole};
21use crate::redis_tcp::{RedisTlsAcceptor, RedisTlsError};
22use crate::services::{DrainOutcome, GracefulShutdown, ServiceSet};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
26#[serde(rename_all = "snake_case")]
27pub enum ServerState {
28 Created,
30 Running,
32 Draining,
34 Stopped,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
40pub struct ServerHealth {
41 pub status: &'static str,
43 pub state: ServerState,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
49pub struct ServerReadiness {
50 pub ready: bool,
52 pub storage_open: bool,
54 pub cluster_ready: bool,
56 pub accepting: bool,
58 pub client_surface_ready: bool,
60 pub redis_surface_ready: bool,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
66pub struct ServerAdminStatus {
67 pub source: StatusSource,
69 pub leader: Option<String>,
71 pub term: u64,
73 pub epoch: u64,
75 pub quorum_ok: bool,
77 pub members: u32,
79 pub member_ids: Vec<String>,
81 pub voters: u32,
83 pub voter_ids: Vec<u64>,
85 pub reshard_phase: String,
87 pub draining: bool,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
93pub struct ServerAdminAction {
94 pub action: &'static str,
96 pub outcome: &'static str,
98 pub detail: String,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct RedisSurfaceRuntime {
105 accepting: bool,
106 active_connections: u64,
107}
108
109impl RedisSurfaceRuntime {
110 fn new() -> Self {
111 Self {
112 accepting: false,
113 active_connections: 0,
114 }
115 }
116
117 fn start(&mut self) {
118 self.accepting = true;
119 }
120
121 fn accepting(&self) -> bool {
122 self.accepting
123 }
124
125 fn begin_connection(&mut self) -> bool {
126 if !self.accepting {
127 return false;
128 }
129 self.active_connections = self.active_connections.saturating_add(1);
130 true
131 }
132
133 fn finish_connection(&mut self) {
134 self.active_connections = self.active_connections.saturating_sub(1);
135 }
136
137 fn active_connections(&self) -> u64 {
138 self.active_connections
139 }
140
141 fn shutdown(&mut self) -> RedisSurfaceDrain {
142 self.accepting = false;
143 let started_with = self.active_connections;
144 self.active_connections = 0;
145 RedisSurfaceDrain {
146 started_with,
147 remaining: self.active_connections,
148 }
149 }
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub struct RedisSurfaceDrain {
155 pub started_with: u64,
157 pub remaining: u64,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
163pub struct ServerObservabilityModel {
164 cluster_grid: ClusterGridCounters,
165 partition_count: u64,
166 configured_default_consistency: Option<String>,
167 backup_age_seconds: Option<u64>,
168 upgrade_phase: String,
169}
170
171impl ServerObservabilityModel {
172 pub fn with_cluster_grid_counters(mut self, counters: ClusterGridCounters) -> Self {
174 self.cluster_grid = counters;
175 self
176 }
177
178 pub fn with_partition_count(mut self, count: u64) -> Self {
180 self.partition_count = count;
181 self
182 }
183
184 pub fn with_configured_default_consistency(mut self, level: impl Into<String>) -> Self {
186 self.configured_default_consistency = Some(level.into());
187 self
188 }
189
190 pub fn with_backup_age_seconds(mut self, seconds: u64) -> Self {
192 self.backup_age_seconds = Some(seconds);
193 self
194 }
195
196 pub fn with_backup_age_seconds_from_namespaces(
198 mut self,
199 ages: impl IntoIterator<Item = u64>,
200 ) -> Self {
201 self.backup_age_seconds = ages.into_iter().max();
202 self
203 }
204
205 pub fn with_upgrade_phase(mut self, phase: impl Into<String>) -> Self {
207 self.upgrade_phase = phase.into();
208 self
209 }
210}
211
212impl Default for ServerObservabilityModel {
213 fn default() -> Self {
214 Self {
215 cluster_grid: ClusterGridCounters::default(),
216 partition_count: 0,
217 configured_default_consistency: None,
218 backup_age_seconds: None,
219 upgrade_phase: "idle".to_owned(),
220 }
221 }
222}
223
224#[derive(Debug, Clone, PartialEq, Eq, Error)]
226pub enum ServerAdminActionError {
227 #[error("server is not ready for admin action: {0}")]
229 NotReady(&'static str),
230 #[error("{0} requires member mode")]
232 RequiresMember(&'static str),
233 #[error("backup admin action requires backup.enabled and backup.location")]
235 BackupDisabled,
236 #[error(transparent)]
238 RaftCompaction(#[from] RaftCompactionError),
239}
240
241#[derive(Debug, Clone)]
243pub struct ServerRuntime {
244 config: ServerConfig,
245 cache: HydraCache,
246 services: ServiceSet,
247 state: ServerState,
248 storage_open: bool,
249 cluster_ready: bool,
250 accepting: bool,
251 flushed: bool,
252 client_surface: Option<ClientSurfaceRuntime>,
253 client_dispatch_state: Option<Arc<hydracache_client_transport_axum::ClientSurfaceState>>,
254 redis_listener_config: Option<RedisListenerConfig>,
255 redis_surface: Option<RedisSurfaceRuntime>,
256 cluster_status: Arc<dyn ClusterStatusProvider>,
257 grid_control: Option<Arc<dyn GridControlPlaneHandle>>,
258 observability: ServerObservabilityModel,
259 last_client_surface_drain: Option<ClientSurfaceDrain>,
260 last_redis_surface_drain: Option<RedisSurfaceDrain>,
261 last_drain: Option<DrainOutcome>,
262}
263
264impl ServerRuntime {
265 pub fn new(config: ServerConfig) -> Result<Self, ServerConfigError> {
267 config.validate()?;
268 let (cache, cluster_status, grid_control): (
269 HydraCache,
270 Arc<dyn ClusterStatusProvider>,
271 Option<Arc<dyn GridControlPlaneHandle>>,
272 ) = match config.role {
273 ServerRole::Member => {
274 let (cache, grid) = crate::grid_host::build_member(&config)?;
275 (
276 cache,
277 Arc::new(LiveClusterStatus::new(Arc::clone(&grid))),
278 Some(grid),
279 )
280 }
281 ServerRole::Local | ServerRole::Client => (
282 HydraCache::local().build(),
283 Arc::new(ModeledClusterStatus),
284 None,
285 ),
286 };
287 let client_surface = if config.client_api.enabled {
288 Some(
289 ClientSurfaceRuntime::new(config.client_api.limits)
290 .map_err(|error| ServerConfigError::InvalidClientApi(error.to_string()))?,
291 )
292 } else {
293 None
294 };
295 let client_dispatch_state = if config.redis_api.enabled || config.hc2_client_plane.enabled {
296 Some(match &client_surface {
297 Some(surface) => surface.state(),
298 None => Arc::new(
299 hydracache_client_transport_axum::ClientSurfaceState::new(
300 config.client_api.limits,
301 )
302 .map_err(|error| ServerConfigError::InvalidClientApi(error.to_string()))?,
303 ),
304 })
305 } else {
306 None
307 };
308 let redis_surface = if config.redis_api.enabled {
309 Some(RedisSurfaceRuntime::new())
310 } else {
311 None
312 };
313 let redis_listener_config = if config.redis_api.enabled {
314 Some(config.redis_listener_config()?)
315 } else {
316 None
317 };
318 Ok(Self {
319 config,
320 cache,
321 services: ServiceSet::default(),
322 state: ServerState::Created,
323 storage_open: false,
324 cluster_ready: false,
325 accepting: false,
326 flushed: false,
327 client_surface,
328 client_dispatch_state,
329 redis_listener_config,
330 redis_surface,
331 cluster_status,
332 grid_control,
333 observability: ServerObservabilityModel::default(),
334 last_client_surface_drain: None,
335 last_redis_surface_drain: None,
336 last_drain: None,
337 })
338 }
339
340 pub fn with_cluster_status_provider(
344 mut self,
345 cluster_status: Arc<dyn ClusterStatusProvider>,
346 ) -> Self {
347 self.cluster_status = cluster_status;
348 self
349 }
350
351 pub fn with_observability_model(mut self, observability: ServerObservabilityModel) -> Self {
353 self.observability = observability;
354 self
355 }
356
357 pub fn start(mut self) -> Self {
359 self.storage_open = true;
360 self.cluster_ready = matches!(
361 self.config.role,
362 ServerRole::Local | ServerRole::Member | ServerRole::Client
363 );
364 self.accepting = true;
365 if let Some(surface) = self.client_surface.as_mut() {
366 surface.start();
367 }
368 if let Some(surface) = self.redis_surface.as_mut() {
369 surface.start();
370 }
371 self.services.start();
372 self.state = ServerState::Running;
373 self
374 }
375
376 pub fn health(&self) -> ServerHealth {
378 ServerHealth {
379 status: if self.state == ServerState::Stopped {
380 "stopped"
381 } else {
382 "ok"
383 },
384 state: self.state,
385 }
386 }
387
388 pub fn ready(&self) -> ServerReadiness {
390 ServerReadiness {
391 ready: self.can_serve(),
392 storage_open: self.storage_open,
393 cluster_ready: self.cluster_ready,
394 accepting: self.accepting,
395 client_surface_ready: self.client_surface_ready(),
396 redis_surface_ready: self.redis_surface_ready(),
397 }
398 }
399
400 pub fn can_serve(&self) -> bool {
402 self.state == ServerState::Running
403 && self.storage_open
404 && self.cluster_ready
405 && self.accepting
406 }
407
408 pub fn is_draining(&self) -> bool {
410 self.state == ServerState::Draining
411 }
412
413 pub fn begin_request(&mut self) -> bool {
415 if !self.accepting {
416 return false;
417 }
418 self.services.begin_request();
419 true
420 }
421
422 pub fn finish_request(&mut self) {
424 self.services.finish_request();
425 }
426
427 pub fn client_surface_ready(&self) -> bool {
429 self.client_surface
430 .as_ref()
431 .is_some_and(ClientSurfaceRuntime::accepting)
432 }
433
434 pub fn begin_client_subscription(&self) -> bool {
436 self.client_surface
437 .as_ref()
438 .is_some_and(|surface| surface.begin_subscription().is_ok())
439 }
440
441 pub fn client_active_subscriptions(&self) -> u64 {
443 self.client_surface
444 .as_ref()
445 .map_or(0, |surface| surface.state().active_subscriptions())
446 }
447
448 pub fn client_surface_drain(&self) -> Option<ClientSurfaceDrain> {
450 self.last_client_surface_drain
451 }
452
453 pub fn redis_surface_ready(&self) -> bool {
455 self.redis_surface
456 .as_ref()
457 .is_some_and(RedisSurfaceRuntime::accepting)
458 }
459
460 pub fn begin_redis_connection(&mut self) -> bool {
462 self.redis_surface
463 .as_mut()
464 .is_some_and(RedisSurfaceRuntime::begin_connection)
465 }
466
467 pub fn finish_redis_connection(&mut self) {
469 if let Some(surface) = self.redis_surface.as_mut() {
470 surface.finish_connection();
471 }
472 }
473
474 pub fn redis_active_connections(&self) -> u64 {
476 self.redis_surface
477 .as_ref()
478 .map_or(0, RedisSurfaceRuntime::active_connections)
479 }
480
481 pub fn redis_surface_drain(&self) -> Option<RedisSurfaceDrain> {
483 self.last_redis_surface_drain
484 }
485
486 pub fn redis_listener_addr(&self) -> Option<SocketAddr> {
488 self.redis_surface
489 .as_ref()
490 .map(|_| self.config.redis_api.listen_addr)
491 }
492
493 pub fn redis_resp_server(&self) -> Result<Option<RedisRespServer>, RedisServeError> {
495 let Some(state) = &self.client_dispatch_state else {
496 return Ok(None);
497 };
498 let Some(config) = &self.redis_listener_config else {
499 return Ok(None);
500 };
501 RedisRespServer::new(Arc::clone(state), config.clone())
502 .map(|server| server.with_native_cache_events(self.cache.clone()))
503 .map(Some)
504 }
505
506 pub fn redis_tls_acceptor(&self) -> Result<Option<RedisTlsAcceptor>, RedisTlsError> {
508 if !self.config.redis_api.enabled || !self.config.redis_api.rediss_enabled {
509 return Ok(None);
510 }
511 RedisTlsAcceptor::from_tls_config(&self.config.tls).map(Some)
512 }
513
514 pub fn client_dispatch_state(
516 &self,
517 ) -> Option<Arc<hydracache_client_transport_axum::ClientSurfaceState>> {
518 self.client_surface
519 .as_ref()
520 .map(ClientSurfaceRuntime::state)
521 .or_else(|| self.client_dispatch_state.as_ref().map(Arc::clone))
522 }
523
524 pub fn diagnostic_reset_enabled(&self) -> bool {
526 self.config.admin_api.diagnostic_reset_enabled
527 }
528
529 pub fn diagnostic_reset_targets(
531 &self,
532 ) -> (
533 HydraCache,
534 Option<Arc<hydracache_client_transport_axum::ClientSurfaceState>>,
535 ) {
536 (self.cache.clone(), self.client_dispatch_state())
537 }
538
539 pub fn begin_drain(&mut self) {
541 self.begin_local_drain();
542 self.cluster_status.begin_drain();
543 }
544
545 pub fn request_admin_drain(&mut self) -> DrainOutcome {
547 if self.state == ServerState::Stopped {
548 return self.last_drain.unwrap_or(DrainOutcome {
549 started_with: 0,
550 remaining: 0,
551 timed_out: false,
552 });
553 }
554 self.begin_local_drain();
555 let drain_timeout = self.config.drain_timeout();
556 let started = Instant::now();
557 let control_plane_drained = self.prepare_cluster_drain(drain_timeout);
558 let mut outcome = GracefulShutdown::new(drain_timeout.saturating_sub(started.elapsed()))
559 .drain(&mut self.services);
560 outcome.timed_out |= !control_plane_drained;
561 self.last_drain = Some(outcome);
562 outcome
563 }
564
565 fn begin_local_drain(&mut self) {
566 if matches!(self.state, ServerState::Stopped) {
567 return;
568 }
569 self.accepting = false;
570 self.state = ServerState::Draining;
571 if let Some(surface) = self.client_surface.as_mut() {
572 if self
573 .last_client_surface_drain
574 .is_none_or(|drain| drain.remaining > 0)
575 {
576 self.last_client_surface_drain = Some(surface.shutdown());
577 }
578 }
579 if let Some(surface) = self.redis_surface.as_mut() {
580 if self
581 .last_redis_surface_drain
582 .is_none_or(|drain| drain.remaining > 0)
583 {
584 self.last_redis_surface_drain = Some(surface.shutdown());
585 }
586 }
587 }
588
589 pub fn graceful_shutdown(&mut self) -> DrainOutcome {
591 if self.state == ServerState::Stopped {
592 return self.last_drain.unwrap_or(DrainOutcome {
593 started_with: 0,
594 remaining: 0,
595 timed_out: false,
596 });
597 }
598 self.begin_local_drain();
599 let drain_timeout = self.config.drain_timeout();
600 let started = Instant::now();
601 let control_plane_drained = self.prepare_cluster_drain(drain_timeout);
602 let mut outcome = GracefulShutdown::new(drain_timeout.saturating_sub(started.elapsed()))
603 .drain(&mut self.services);
604 outcome.timed_out |= !control_plane_drained;
605 self.flushed = true;
606 self.storage_open = false;
607 self.cluster_ready = false;
608 self.services.stop();
609 self.state = ServerState::Stopped;
610 self.last_drain = Some(outcome);
611 outcome
612 }
613
614 pub fn shutdown(&mut self) -> DrainOutcome {
616 self.graceful_shutdown()
617 }
618
619 fn leave_cluster_for_shutdown(&self) {
620 if matches!(self.config.role, ServerRole::Member | ServerRole::Client) {
621 let _ = block_on_cluster_leave(&self.cache);
622 }
623 }
624
625 fn prepare_cluster_drain(&self, timeout: std::time::Duration) -> bool {
626 self.leave_cluster_for_shutdown();
632 self.cluster_status.begin_drain();
633 self.cluster_status.wait_for_drain_ready(timeout)
634 }
635
636 pub fn admin_status(&self) -> ServerAdminStatus {
638 let status = self.cluster_status_snapshot();
639 let mut member_ids = status
640 .members
641 .iter()
642 .map(|member| member.node_id.clone())
643 .collect::<Vec<_>>();
644 member_ids.sort();
645 ServerAdminStatus {
646 source: status.source,
647 leader: status.leader,
648 term: status.term,
649 epoch: status.epoch,
650 quorum_ok: status.quorum_ok,
651 members: status.members.len() as u32,
652 member_ids,
653 voters: status.voters,
654 voter_ids: status.voter_ids,
655 reshard_phase: status.reshard_phase.to_string(),
656 draining: status.draining,
657 }
658 }
659
660 pub fn raft_compaction_status(&self) -> Result<RaftCompactionStatus, RaftCompactionError> {
662 self.grid_control.as_ref().map_or_else(
663 || Ok(RaftCompactionStatus::unavailable()),
664 |grid| grid.raft_compaction_status(),
665 )
666 }
667
668 pub fn request_raft_compaction(&self) -> Result<RaftCompactionStatus, ServerAdminActionError> {
670 if !self.can_serve() {
671 return Err(ServerAdminActionError::NotReady("raft compaction"));
672 }
673 if !matches!(self.config.role, ServerRole::Member) {
674 return Err(ServerAdminActionError::RequiresMember("raft compaction"));
675 }
676 self.grid_control
677 .as_ref()
678 .ok_or(RaftCompactionError::Unavailable)?
679 .compact_raft_log_at_applied()
680 .map_err(ServerAdminActionError::from)
681 }
682
683 pub fn metrics_registry(&self) -> HydraCacheRegistry {
685 let status = self.cluster_status_snapshot();
686 let registry = HydraCacheRegistry::new()
687 .with_cache("server", self.cache.clone())
688 .with_cluster_grid_counters(self.observability.cluster_grid)
689 .with_topology(ClusterTopologyOverview::new(
690 topology_status_source(status.source),
691 status.members.len() as u64,
692 status.leader,
693 status.epoch,
694 topology_reshard_phase(status.reshard_phase),
695 ));
696 if let Some(seconds) = self.observability.backup_age_seconds {
697 registry.with_backup_age_seconds(seconds)
698 } else {
699 registry
700 }
701 }
702
703 pub fn cluster_overview(&self) -> ClusterOverview {
705 let status = self.cluster_status_snapshot();
706 let counters = overview_cluster_grid_counters(
707 self.cache.cluster_grid_counters(),
708 self.observability.cluster_grid,
709 );
710 ClusterOverview::new(
711 topology_status_source(status.source),
712 status
713 .members
714 .iter()
715 .map(|member| {
716 ClusterMemberView::new(
717 member.node_id.clone(),
718 member_role_label(member.role),
719 member.reachable == Reachability::Reachable,
720 reachability_label(member.reachable),
721 member.generation,
722 )
723 })
724 .collect(),
725 cluster_overview_leader(&status),
726 PartitionSummary::from_grid_counters(counters, self.observability.partition_count),
727 ConsistencyView::from_grid_counters(
728 self.observability.configured_default_consistency.clone(),
729 counters,
730 ),
731 self.observability.backup_age_seconds,
732 LifecycleView::new(
733 status.reshard_phase.to_string(),
734 self.observability.upgrade_phase.clone(),
735 ),
736 )
737 }
738
739 fn cluster_status_snapshot(&self) -> ClusterStatus {
740 let cluster_ready = self.cluster_ready && self.state != ServerState::Stopped;
741 self.cluster_status
742 .cluster_status(ClusterStatusRuntime::new(cluster_ready, self.is_draining()))
743 }
744
745 pub fn request_reshard(&self) -> Result<ServerAdminAction, ServerAdminActionError> {
747 if !self.can_serve() {
748 return Err(ServerAdminActionError::NotReady("reshard"));
749 }
750 if !matches!(self.config.role, ServerRole::Member) {
751 return Err(ServerAdminActionError::RequiresMember("reshard"));
752 }
753 Ok(ServerAdminAction {
754 action: "reshard",
755 outcome: "accepted",
756 detail: "reshard request accepted by member runtime".to_owned(),
757 })
758 }
759
760 pub fn request_backup(&self) -> Result<ServerAdminAction, ServerAdminActionError> {
762 if !self.can_serve() {
763 return Err(ServerAdminActionError::NotReady("backup"));
764 }
765 if !self.config.backup.enabled
766 || self
767 .config
768 .backup
769 .location
770 .as_deref()
771 .unwrap_or("")
772 .trim()
773 .is_empty()
774 {
775 return Err(ServerAdminActionError::BackupDisabled);
776 }
777 Ok(ServerAdminAction {
778 action: "backup",
779 outcome: "accepted",
780 detail: "backup request accepted by configured runtime".to_owned(),
781 })
782 }
783
784 pub fn flushed(&self) -> bool {
786 self.flushed
787 }
788
789 pub fn cache(&self) -> &HydraCache {
791 &self.cache
792 }
793
794 pub fn config(&self) -> &ServerConfig {
796 &self.config
797 }
798}
799
800fn topology_status_source(source: StatusSource) -> TopologyStatusSource {
801 match source {
802 StatusSource::Live => TopologyStatusSource::Live,
803 StatusSource::Modeled => TopologyStatusSource::Modeled,
804 }
805}
806
807fn block_on_cluster_leave(cache: &HydraCache) -> hydracache::CacheResult<()> {
808 let cache = cache.clone();
809 if tokio::runtime::Handle::try_current().is_ok() {
810 return std::thread::spawn(move || block_on_cluster_leave_without_current(cache))
811 .join()
812 .map_err(|_| {
813 hydracache::CacheError::Backend("cluster leave helper thread panicked".to_owned())
814 })?;
815 }
816
817 block_on_cluster_leave_without_current(cache)
818}
819
820fn block_on_cluster_leave_without_current(cache: HydraCache) -> hydracache::CacheResult<()> {
821 let runtime = tokio::runtime::Builder::new_current_thread()
822 .enable_all()
823 .build()
824 .map_err(|error| {
825 hydracache::CacheError::Backend(format!(
826 "failed to build cluster leave runtime: {error}"
827 ))
828 })?;
829 let left = runtime.block_on(cache.leave_cluster())?;
830 let _ = left;
831 Ok(())
832}
833
834fn topology_reshard_phase(phase: ReshardPhase) -> TopologyReshardPhase {
835 match phase {
836 ReshardPhase::Idle => TopologyReshardPhase::Idle,
837 ReshardPhase::Planning => TopologyReshardPhase::Planning,
838 ReshardPhase::Moving => TopologyReshardPhase::Moving,
839 ReshardPhase::Finalizing => TopologyReshardPhase::Finalizing,
840 }
841}
842
843fn cluster_overview_leader(status: &ClusterStatus) -> Option<LeaderView> {
844 if status.source != StatusSource::Live || !status.quorum_ok {
845 return None;
846 }
847 status
848 .leader
849 .as_ref()
850 .map(|node_id| LeaderView::new(node_id.clone(), status.term, status.epoch))
851}
852
853fn member_role_label(role: MemberRole) -> &'static str {
854 match role {
855 MemberRole::Local => "local",
856 MemberRole::Client => "client",
857 MemberRole::Member => "member",
858 }
859}
860
861fn reachability_label(reachability: Reachability) -> &'static str {
862 match reachability {
863 Reachability::Reachable => "reachable",
864 Reachability::Suspect => "suspect",
865 Reachability::Unreachable => "unreachable",
866 }
867}
868
869fn overview_cluster_grid_counters(
870 mut left: ClusterGridCounters,
871 right: ClusterGridCounters,
872) -> ClusterGridCounters {
873 left.under_replicated_keys = left
874 .under_replicated_keys
875 .saturating_add(right.under_replicated_keys);
876 left.consistency_level_operations_total = left
877 .consistency_level_operations_total
878 .saturating_add(right.consistency_level_operations_total);
879 left
880}