use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use hydracache::{ClusterMember, ClusterNodeId, ClusterRole, RaftMetadataSnapshot};
use serde::Serialize;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ClusterStatusRuntime {
pub ready: bool,
pub draining: bool,
}
impl ClusterStatusRuntime {
pub fn new(ready: bool, draining: bool) -> Self {
Self { ready, draining }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum StatusSource {
Live,
Modeled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MemberRole {
Local,
Client,
Member,
}
impl From<ClusterRole> for MemberRole {
fn from(value: ClusterRole) -> Self {
match value {
ClusterRole::Local => Self::Local,
ClusterRole::Client => Self::Client,
ClusterRole::Member => Self::Member,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Reachability {
Reachable,
Suspect,
Unreachable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ReshardPhase {
Idle,
Planning,
Moving,
Finalizing,
}
impl fmt::Display for ReshardPhase {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let value = match self {
Self::Idle => "idle",
Self::Planning => "planning",
Self::Moving => "moving",
Self::Finalizing => "finalizing",
};
formatter.write_str(value)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct MemberStatus {
pub node_id: String,
pub role: MemberRole,
pub reachable: Reachability,
pub generation: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ClusterStatus {
pub source: StatusSource,
pub leader: Option<String>,
pub term: u64,
pub epoch: u64,
pub quorum_ok: bool,
pub members: Vec<MemberStatus>,
pub voters: u32,
pub voter_ids: Vec<u64>,
pub reshard_phase: ReshardPhase,
pub draining: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct RaftCompactionStatus {
pub available: bool,
pub enabled: bool,
pub applied_index: Option<u64>,
pub snapshot_index: Option<u64>,
pub first_log_index: Option<u64>,
pub last_log_index: Option<u64>,
pub snapshot_send_attempts: Option<u64>,
pub snapshot_send_successes: Option<u64>,
pub snapshot_send_failures: Option<u64>,
pub snapshot_sends_in_flight: Option<u64>,
pub snapshot_sender_tasks_current: Option<u64>,
pub snapshot_sender_tasks_high_water: Option<u64>,
pub snapshot_installs: Option<u64>,
}
impl RaftCompactionStatus {
pub(crate) fn unavailable() -> Self {
Self {
available: false,
enabled: false,
applied_index: None,
snapshot_index: None,
first_log_index: None,
last_log_index: None,
snapshot_send_attempts: None,
snapshot_send_successes: None,
snapshot_send_failures: None,
snapshot_sends_in_flight: None,
snapshot_sender_tasks_current: None,
snapshot_sender_tasks_high_water: None,
snapshot_installs: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum RaftCompactionError {
#[error("raft compaction control is unavailable for this runtime")]
Unavailable,
#[error("raft compaction control is disabled; set HYDRACACHE_RAFT_COMPACTION=true explicitly")]
Disabled,
#[error("raft compaction failed: {0}")]
Runtime(String),
}
pub trait ClusterStatusProvider: fmt::Debug + Send + Sync {
fn begin_drain(&self) {}
fn wait_for_drain_ready(&self, _timeout: Duration) -> bool {
true
}
fn cluster_status(&self, runtime: ClusterStatusRuntime) -> ClusterStatus;
}
#[derive(Debug, Clone, Default)]
pub struct ModeledClusterStatus;
impl ClusterStatusProvider for ModeledClusterStatus {
fn cluster_status(&self, runtime: ClusterStatusRuntime) -> ClusterStatus {
ClusterStatus {
source: StatusSource::Modeled,
leader: runtime.ready.then(|| "local".to_owned()),
term: u64::from(runtime.ready),
epoch: 0,
quorum_ok: runtime.ready && !runtime.draining,
members: Vec::new(),
voters: 0,
voter_ids: Vec::new(),
reshard_phase: ReshardPhase::Idle,
draining: runtime.draining,
}
}
}
pub trait GridControlPlaneHandle: fmt::Debug + Send + Sync {
fn begin_drain(&self);
fn wait_for_drain_ready(&self, _timeout: Duration) -> bool {
true
}
fn snapshot(&self) -> RaftMetadataSnapshot;
fn members(&self) -> Vec<ClusterMember>;
fn raft_leader_id(&self) -> Option<String>;
fn has_quorum(&self) -> bool;
fn metadata_authority_matches(&self, observed: &RaftMetadataSnapshot) -> bool {
let _ = observed;
true
}
fn voter_count(&self) -> u32;
fn voter_ids(&self) -> Vec<u64>;
fn reachability(&self, node: &ClusterNodeId) -> Reachability;
fn reshard_phase(&self) -> ReshardPhase;
fn is_draining(&self) -> bool;
fn raft_compaction_status(&self) -> Result<RaftCompactionStatus, RaftCompactionError> {
Ok(RaftCompactionStatus::unavailable())
}
fn compact_raft_log_at_applied(&self) -> Result<RaftCompactionStatus, RaftCompactionError> {
Err(RaftCompactionError::Unavailable)
}
}
#[derive(Debug, Clone)]
pub struct LiveClusterStatus {
grid: Arc<dyn GridControlPlaneHandle>,
}
impl LiveClusterStatus {
pub fn new(grid: Arc<dyn GridControlPlaneHandle>) -> Self {
Self { grid }
}
}
impl ClusterStatusProvider for LiveClusterStatus {
fn begin_drain(&self) {
self.grid.begin_drain();
}
fn wait_for_drain_ready(&self, timeout: Duration) -> bool {
self.grid.wait_for_drain_ready(timeout)
}
fn cluster_status(&self, runtime: ClusterStatusRuntime) -> ClusterStatus {
let snapshot = self.grid.snapshot();
let draining = runtime.draining || self.grid.is_draining();
let members = self
.grid
.members()
.into_iter()
.map(|member| MemberStatus {
node_id: member.node_id.to_string(),
role: MemberRole::from(member.role),
reachable: self.grid.reachability(&member.node_id),
generation: member.generation.value(),
})
.collect();
let metadata_authoritative = self.grid.metadata_authority_matches(&snapshot);
ClusterStatus {
source: StatusSource::Live,
leader: metadata_authoritative
.then(|| self.grid.raft_leader_id())
.flatten(),
term: snapshot.term,
epoch: snapshot.epoch.value(),
quorum_ok: runtime.ready
&& metadata_authoritative
&& self.grid.has_quorum()
&& !draining,
members,
voters: self.grid.voter_count(),
voter_ids: self.grid.voter_ids(),
reshard_phase: self.grid.reshard_phase(),
draining,
}
}
}