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}