photon_etcd_cluster/
types.rs

1use std::{net::IpAddr, sync::atomic::{AtomicU8, Ordering}};
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6/// Schema-less metadata for nodes.
7///
8/// This type alias provides maximum flexibility for arbitrary node metadata.
9/// Nodes can report any JSON-serializable data (CPU load, memory, custom metrics, etc.)
10/// and consumers (like load balancers) can interpret the metadata as needed.
11///
12/// # Common Metadata Keys
13///
14/// While the schema is flexible, these keys are commonly used:
15/// - `cpu_usage_percent`: CPU usage as percentage (0.0 - 100.0)
16/// - `memory_usage_percent`: Memory usage as percentage (0.0 - 100.0)
17/// - `memory_available_bytes`: Available memory in bytes
18/// - `load_avg_1m`: 1-minute load average
19/// - `active_connections`: Number of active connections
20/// - `requests_per_second`: Request rate
21/// - `queue_depth`: Request queue depth
22///
23/// # Example
24///
25/// ```ignore
26/// use serde_json::json;
27///
28/// let metadata = json!({
29///     "cpu_usage_percent": 45.5,
30///     "memory_usage_percent": 62.0,
31///     "custom_metric": "value"
32/// });
33/// ```
34pub type NodeMetadata = Value;
35
36/// Well-known metadata keys for convenience.
37pub mod metadata_keys {
38    /// CPU usage as percentage (0.0 - 100.0)
39    pub const CPU_USAGE_PERCENT: &str = "cpu_usage_percent";
40    /// Memory usage as percentage (0.0 - 100.0)
41    pub const MEMORY_USAGE_PERCENT: &str = "memory_usage_percent";
42    /// Available memory in bytes
43    pub const MEMORY_AVAILABLE_BYTES: &str = "memory_available_bytes";
44    /// 1-minute load average
45    pub const LOAD_AVG_1M: &str = "load_avg_1m";
46    /// Active connections/requests being processed
47    pub const ACTIVE_CONNECTIONS: &str = "active_connections";
48    /// Requests per second
49    pub const REQUESTS_PER_SECOND: &str = "requests_per_second";
50    /// Request queue depth
51    pub const QUEUE_DEPTH: &str = "queue_depth";
52}
53
54/// Represents a node in the cluster.
55///
56/// Each node has a unique identifier, network address, and timestamp
57/// of when it was last seen by the cluster.
58///
59/// # Equality
60///
61/// Two nodes are considered equal if they have the same `id` and `ip`.
62/// The `last_seen` timestamp and `metadata` are intentionally excluded from
63/// equality checks to allow detecting actual state changes vs heartbeat updates.
64///
65/// # Metadata
66///
67/// The optional `metadata` field allows nodes to report arbitrary data
68/// (CPU load, memory, custom metrics) that can be used by load balancers
69/// for weighted routing decisions.
70#[derive(Serialize, Deserialize, Debug, Clone, Eq)]
71pub struct Node {
72    /// Unique identifier for this node
73    pub id: String,
74    /// Network address of this node
75    pub ip: IpAddr,
76    /// Unix timestamp (milliseconds) when this node was last seen
77    pub last_seen: i64,
78    /// Optional node metadata (CPU, memory, custom metrics)
79    /// Defaults to null if not provided (backward compatible)
80    #[serde(default, skip_serializing_if = "Value::is_null")]
81    pub metadata: NodeMetadata,
82}
83
84impl PartialEq for Node {
85    fn eq(&self, other: &Self) -> bool {
86        self.id == other.id && self.ip == other.ip
87    }
88}
89
90impl std::hash::Hash for Node {
91    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
92        self.id.hash(state);
93        self.ip.hash(state);
94    }
95}
96
97/// Health status of a cluster node's connection to etcd.
98#[repr(u8)]
99#[derive(Debug, PartialEq, Clone, Copy)]
100pub enum HealthStatus {
101    /// Initial state before first health check
102    Unknown = 0,
103    /// Connected and heartbeats are successful
104    Healthy = 1,
105    /// Connection issues or missed heartbeats
106    Unhealthy = 2,
107}
108
109/// Thread-safe atomic wrapper for [`HealthStatus`].
110#[derive(Debug)]
111pub struct AtomicHealth(AtomicU8);
112
113impl Default for AtomicHealth {
114    fn default() -> Self {
115        Self::new()
116    }
117}
118
119impl AtomicHealth {
120    /// Creates a new `AtomicHealth` with `Unknown` status.
121    pub fn new() -> Self {
122        Self(AtomicU8::new(HealthStatus::Unknown as u8))
123    }
124
125    /// Loads the current health status.
126    pub fn load(&self) -> HealthStatus {
127        match self.0.load(Ordering::Acquire) {
128            1 => HealthStatus::Healthy,
129            2 => HealthStatus::Unhealthy,
130            _ => HealthStatus::Unknown,
131        }
132    }
133
134    /// Stores a new health status.
135    pub fn store(&self, status: HealthStatus) {
136        self.0.store(status as u8, Ordering::Release);
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_atomic_health_state_transitions() {
146        let health = AtomicHealth::new();
147        assert_eq!(health.load(), HealthStatus::Unknown);
148
149        health.store(HealthStatus::Healthy);
150        assert_eq!(health.load(), HealthStatus::Healthy);
151
152        health.store(HealthStatus::Unhealthy);
153        assert_eq!(health.load(), HealthStatus::Unhealthy);
154
155        // Can transition back to healthy
156        health.store(HealthStatus::Healthy);
157        assert_eq!(health.load(), HealthStatus::Healthy);
158    }
159
160    #[test]
161    fn test_atomic_health_default() {
162        let health = AtomicHealth::default();
163        assert_eq!(health.load(), HealthStatus::Unknown);
164    }
165}