Skip to main content

hydracache_server/
cluster_status.rs

1//! Honest cluster-status seam for the admin and Management Center surfaces.
2
3use std::fmt;
4use std::sync::Arc;
5
6use hydracache::{ClusterMember, ClusterNodeId, ClusterRole, RaftMetadataSnapshot};
7use serde::Serialize;
8
9/// Runtime state supplied by the server around the cluster-status provider.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct ClusterStatusRuntime {
12    /// Whether the server runtime is ready to serve.
13    pub ready: bool,
14    /// Whether the server runtime is draining.
15    pub draining: bool,
16}
17
18impl ClusterStatusRuntime {
19    /// Build a runtime status input.
20    pub fn new(ready: bool, draining: bool) -> Self {
21        Self { ready, draining }
22    }
23}
24
25/// Where a status reading came from.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
27#[serde(rename_all = "snake_case")]
28pub enum StatusSource {
29    /// Status came from a live grid/control-plane handle.
30    Live,
31    /// Status is the local daemon model and must not be painted as live.
32    Modeled,
33}
34
35/// Role rendered for a cluster member in management views.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
37#[serde(rename_all = "snake_case")]
38pub enum MemberRole {
39    /// Local, non-clustered runtime.
40    Local,
41    /// Client near-cache runtime.
42    Client,
43    /// Voting/cache member runtime.
44    Member,
45}
46
47impl From<ClusterRole> for MemberRole {
48    fn from(value: ClusterRole) -> Self {
49        match value {
50            ClusterRole::Local => Self::Local,
51            ClusterRole::Client => Self::Client,
52            ClusterRole::Member => Self::Member,
53        }
54    }
55}
56
57/// Reachability state reported for a known member.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
59#[serde(rename_all = "snake_case")]
60pub enum Reachability {
61    /// Member is currently reachable.
62    Reachable,
63    /// Member is suspected but not yet declared unreachable.
64    Suspect,
65    /// Member is unreachable and must remain visible in the view.
66    Unreachable,
67}
68
69/// Coarse reshard lifecycle phase for read-only status surfaces.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
71#[serde(rename_all = "snake_case")]
72pub enum ReshardPhase {
73    /// No reshard is active.
74    Idle,
75    /// A reshard is being planned.
76    Planning,
77    /// Partitions or replicas are moving.
78    Moving,
79    /// The reshard is finalizing.
80    Finalizing,
81}
82
83impl fmt::Display for ReshardPhase {
84    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
85        let value = match self {
86            Self::Idle => "idle",
87            Self::Planning => "planning",
88            Self::Moving => "moving",
89            Self::Finalizing => "finalizing",
90        };
91        formatter.write_str(value)
92    }
93}
94
95/// Read-only member status shown by the Management Center.
96#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
97pub struct MemberStatus {
98    /// Stable logical node id.
99    pub node_id: String,
100    /// Runtime role.
101    pub role: MemberRole,
102    /// Current reachability.
103    pub reachable: Reachability,
104    /// Process generation.
105    pub generation: u64,
106}
107
108/// Read-only cluster status snapshot.
109#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
110pub struct ClusterStatus {
111    /// Whether the snapshot is live or modeled.
112    pub source: StatusSource,
113    /// Current leader id, if a live raft source knows one.
114    pub leader: Option<String>,
115    /// Current control-plane term.
116    pub term: u64,
117    /// Current authority epoch.
118    pub epoch: u64,
119    /// Whether quorum is available while the runtime is not draining.
120    pub quorum_ok: bool,
121    /// Visible members. Unreachable members remain present.
122    pub members: Vec<MemberStatus>,
123    /// Current raft voter count, if known.
124    pub voters: u32,
125    /// Current reshard phase.
126    pub reshard_phase: ReshardPhase,
127    /// Whether the runtime is draining.
128    pub draining: bool,
129}
130
131/// Read-only provider of cluster status.
132pub trait ClusterStatusProvider: fmt::Debug + Send + Sync {
133    /// Notify the provider that the server has started graceful drain.
134    fn begin_drain(&self) {}
135
136    /// Return a cluster status snapshot, incorporating current runtime state.
137    fn cluster_status(&self, runtime: ClusterStatusRuntime) -> ClusterStatus;
138}
139
140/// Modeled status used when the daemon has no live grid handle.
141#[derive(Debug, Clone, Default)]
142pub struct ModeledClusterStatus;
143
144impl ClusterStatusProvider for ModeledClusterStatus {
145    fn cluster_status(&self, runtime: ClusterStatusRuntime) -> ClusterStatus {
146        ClusterStatus {
147            source: StatusSource::Modeled,
148            leader: runtime.ready.then(|| "local".to_owned()),
149            term: u64::from(runtime.ready),
150            epoch: 0,
151            quorum_ok: runtime.ready && !runtime.draining,
152            members: Vec::new(),
153            voters: 0,
154            reshard_phase: ReshardPhase::Idle,
155            draining: runtime.draining,
156        }
157    }
158}
159
160/// Minimal read-only handle over a live grid/control-plane.
161pub trait GridControlPlaneHandle: fmt::Debug + Send + Sync {
162    /// Notify the live grid that graceful drain started.
163    fn begin_drain(&self);
164
165    /// Return a point-in-time control-plane snapshot.
166    fn snapshot(&self) -> RaftMetadataSnapshot;
167    /// Return visible members.
168    fn members(&self) -> Vec<ClusterMember>;
169    /// Return the raft soft-state leader id, if known.
170    fn raft_leader_id(&self) -> Option<String>;
171    /// Return whether the live grid currently has quorum.
172    fn has_quorum(&self) -> bool;
173    /// Return current raft voter count.
174    fn voter_count(&self) -> u32;
175    /// Return reachability for one known node.
176    fn reachability(&self, node: &ClusterNodeId) -> Reachability;
177    /// Return the current reshard phase.
178    fn reshard_phase(&self) -> ReshardPhase;
179    /// Return whether the grid itself is draining.
180    fn is_draining(&self) -> bool;
181}
182
183/// Live status backed by a grid/control-plane handle.
184#[derive(Debug, Clone)]
185pub struct LiveClusterStatus {
186    grid: Arc<dyn GridControlPlaneHandle>,
187}
188
189impl LiveClusterStatus {
190    /// Build a live status provider.
191    pub fn new(grid: Arc<dyn GridControlPlaneHandle>) -> Self {
192        Self { grid }
193    }
194}
195
196impl ClusterStatusProvider for LiveClusterStatus {
197    fn begin_drain(&self) {
198        self.grid.begin_drain();
199    }
200
201    fn cluster_status(&self, runtime: ClusterStatusRuntime) -> ClusterStatus {
202        let snapshot = self.grid.snapshot();
203        let draining = runtime.draining || self.grid.is_draining();
204        let members = self
205            .grid
206            .members()
207            .into_iter()
208            .map(|member| MemberStatus {
209                node_id: member.node_id.to_string(),
210                role: MemberRole::from(member.role),
211                reachable: self.grid.reachability(&member.node_id),
212                generation: member.generation.value(),
213            })
214            .collect();
215
216        ClusterStatus {
217            source: StatusSource::Live,
218            leader: self.grid.raft_leader_id(),
219            term: snapshot.term,
220            epoch: snapshot.epoch.value(),
221            quorum_ok: runtime.ready && self.grid.has_quorum() && !draining,
222            members,
223            voters: self.grid.voter_count(),
224            reshard_phase: self.grid.reshard_phase(),
225            draining,
226        }
227    }
228}