use std::collections::{BTreeMap, HashMap};
use std::sync::{Arc, Mutex, RwLock};
use super::node_manager::{NodeMetadata, NodeStatus};
use tokio::time::interval;
use tracing::{debug, info, warn};
use std::time::{Duration, Instant};
use std::time::SystemTime;
use std::hash::Hash;
#[derive(Debug, Clone)]
pub struct DiscoveryConfig {
pub cluster_name: String,
pub gossip_interval: Duration,
pub node_timeout: Duration,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct NodeInfo {
pub id: String,
pub address: String,
pub cpu_cores: usize,
pub memory_gb: usize,
pub location: String,
pub capabilities: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct GossipMessage {
pub cluster_name: String,
pub node_id: String,
pub node_info: NodeInfo,
pub timestamp: u64, }
#[derive(Debug, Clone)]
pub struct ServiceDiscovery {
config: DiscoveryConfig,
nodes: Arc<RwLock<HashMap<String, NodeMetadata>>>,
gossip_history: Arc<RwLock<Vec<GossipMessage>>>,
}
impl ServiceDiscovery {
pub fn new(config: DiscoveryConfig) -> Self {
let discovery: _ = Self {
config: config.clone(),
nodes: Arc::new(Mutex::new(HashMap::new())),
gossip_history: Arc::new(Mutex::new(Vec::new())),
};
let config_clone: _ = config.clone();
let nodes_clone: _ = discovery.nodes.clone();
let gossip_history_clone: _ = discovery.gossip_history.clone();
tokio::spawn(async move {
let mut interval = interval(config_clone.gossip_interval);
loop {
interval.tick().await;
Self::gossip_protocol(
nodes_clone.clone(),
gossip_history_clone.clone(),
&config_clone,
).await;
}
});
discovery
}
pub async fn register_self(&self, node_info: NodeInfo) {
let mut nodes = self.nodes.write().await;
let metadata: _ = NodeMetadata {
cpu_cores: node_info.cpu_cores,
memory_gb: node_info.memory_gb,
location: node_info.location.clone(),
capabilities: node_info.capabilities.clone(),
status: NodeStatus::Online,
registered_at: Instant::now(),
last_heartbeat: Instant::now(),
version: 1,
};
nodes.insert(node_info.id.clone(), metadata);
info!("Registered self node: {}", node_info.id);
self.broadcast_gossip(&node_info).await;
}
pub async fn update_node(&self, node_info: NodeInfo) -> Result<(), String> {
let mut nodes = self.nodes.write().await;
if let Some(metadata) = nodes.get_mut(&node_info.id) {
metadata.last_heartbeat = Instant::now();
metadata.version += 1;
info!("Updated node: {}", node_info.id);
} else {
let metadata: _ = NodeMetadata {
cpu_cores: node_info.cpu_cores,
memory_gb: node_info.memory_gb,
location: node_info.location.clone(),
capabilities: node_info.capabilities.clone(),
status: NodeStatus::Online,
registered_at: Instant::now(),
last_heartbeat: Instant::now(),
version: 1,
};
nodes.insert(node_info.id.clone(), metadata);
info!("Registered new node: {}", node_info.id);
}
self.broadcast_gossip(&node_info).await;
Ok(())
}
pub async fn get_known_nodes(&self) -> Vec<NodeInfo> {
let nodes: _ = self.nodes.read().await;
let known_nodes: _ = Vec::new();
let now: _ = Instant::now();
let timeout_nodes: Vec<String> = nodes
.iter()
.filter(|(_, metadata)| now.duration_since(metadata.last_heartbeat) > self.config.node_timeout)
.map(|(id, _)| id.clone())
.collect();
for timeout_node in timeout_nodes {
warn!("Node timeout: {}", timeout_node);
}
for (id, metadata) in nodes.iter() {
if now.duration_since(metadata.last_heartbeat) <= self.config.node_timeout {
debug!("Active node: {}", id);
}
}
known_nodes
}
async fn gossip_protocol(
nodes: Arc<RwLock<HashMap<String, NodeMetadata>>>,
_gossip_history: Arc<RwLock<Vec<GossipMessage>>>,
_config: &DiscoveryConfig,
) {
let known_nodes: _ = {
let nodes_guard: _ = nodes.read().await;
nodes_guard.keys().cloned().collect::<Vec<_>>()
};
if known_nodes.len() < 2 {
return; }
let mut rng = rand::rng();
let target_count: _ = (known_nodes.len() / 3).max(1);
for _ in 0..target_count {
if let Some(target) = known_nodes.iter().choose(&mut rng) {
debug!("Gossiping with node: {}", target);
}
}
}
async fn broadcast_gossip(&self, node_info: &NodeInfo) {
let message: _ = GossipMessage {
cluster_name: self.config.cluster_name.clone(),
node_id: node_info.id.clone(),
node_info: node_info.clone(),
timestamp: std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(),
};
let mut history = self.gossip_history.write().await;
history.push(message);
if history.len() > 1000 {
history.drain(0..100);
}
}
pub async fn receive_gossip(&self, message: GossipMessage) {
if message.cluster_name != self.config.cluster_name {
return; }
let node_id: _ = message.node_id.clone();
let mut nodes = self.nodes.write().await;
let metadata: _ = NodeMetadata {
cpu_cores: message.node_info.cpu_cores,
memory_gb: message.node_info.memory_gb,
location: message.node_info.location.clone(),
capabilities: message.node_info.capabilities.clone(),
status: NodeStatus::Online,
registered_at: Instant::now(),
last_heartbeat: Instant::now(),
version: 1,
};
nodes.insert(node_id.clone(), metadata);
debug!("Received gossip from: {}", node_id);
}
pub async fn cleanup_offline_nodes(&self) -> usize {
let mut nodes = self.nodes.write().await;
let now: _ = Instant::now();
let offline_nodes: Vec<String> = nodes
.iter()
.filter(|(_, metadata)| now.duration_since(metadata.last_heartbeat) > self.config.node_timeout)
.map(|(id, _)| id.clone())
.collect();
for node_id in &offline_nodes {
nodes.remove(node_id);
warn!("Removed offline node: {}", node_id);
}
offline_nodes.len()
}
pub async fn get_cluster_stats(&self) -> ClusterStats {
let nodes: _ = self.nodes.read().await;
let now: _ = Instant::now();
let (online, offline, total) = {
let mut online_count = 0;
let mut offline_count = 0;
for (_, metadata) in nodes.iter() {
if now.duration_since(metadata.last_heartbeat) <= self.config.node_timeout {
online_count += 1;
} else {
offline_count += 1;
}
}
(online_count, offline_count, nodes.len())
};
ClusterStats {
total_nodes: total,
online_nodes: online,
offline_nodes: offline,
cluster_name: self.config.cluster_name.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct ClusterStats {
pub total_nodes: usize,
pub online_nodes: usize,
pub offline_nodes: usize,
pub cluster_name: String,
}
impl ClusterStats {
pub fn is_healthy(&self) -> bool {
self.online_nodes > 0 && (self.offline_nodes as f64 / self.total_nodes as f64) < 0.5
}
pub fn availability_percentage(&self) -> f64 {
if self.total_nodes == 0 {
return 0.0;
}
(self.online_nodes as f64 / self.total_nodes as f64) * 100.0
}
}
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_service_discovery_registration() {
let config: _ = DiscoveryConfig {
cluster_name: "test-cluster".to_string(),
gossip_interval: Duration::from_millis(100),
node_timeout: Duration::from_secs(5),
};
let discovery: _ = ServiceDiscovery::new(config);
let node: _ = NodeInfo {
id: "test-node".to_string(),
address: "192.168.1.1:8080".to_string(),
cpu_cores: 4,
memory_gb: 8,
location: "us-west".to_string(),
capabilities: vec!["js-execution".to_string()],
};
discovery.register_self(node.clone()).await;
tokio::time::sleep(Duration::from_millis(200)).await;
let stats: _ = discovery.get_cluster_stats().await;
assert_eq!(stats.total_nodes, 1);
assert_eq!(stats.online_nodes, 1);
}
#[tokio::test]
async fn test_node_cleanup() {
let config: _ = DiscoveryConfig {
cluster_name: "test-cluster".to_string(),
gossip_interval: Duration::from_millis(100),
node_timeout: Duration::from_millis(300),
};
let discovery: _ = ServiceDiscovery::new(config);
let node: _ = NodeInfo {
id: "test-node".to_string(),
address: "192.168.1.1:8080".to_string(),
cpu_cores: 4,
memory_gb: 8,
location: "us-west".to_string(),
capabilities: vec!["js-execution".to_string()],
};
discovery.register_self(node).await;
tokio::time::sleep(Duration::from_millis(400)).await;
let cleaned_count: _ = discovery.cleanup_offline_nodes().await;
assert_eq!(cleaned_count, 1);
let stats: _ = discovery.get_cluster_stats().await;
assert_eq!(stats.total_nodes, 0);
}
}