Skip to main content

mecha10_dev/services/discovery/
mod.rs

1#![allow(dead_code)]
2
3//! Discovery service for finding Mecha10 nodes on the network
4//!
5//! This service provides operations for discovering and monitoring Mecha10
6//! nodes through Redis pub/sub and network scanning.
7
8mod node_manager;
9mod parser;
10mod redis_ops;
11
12use anyhow::Result;
13use std::collections::HashMap;
14use std::time::{Duration, SystemTime};
15
16/// Information about a discovered node
17#[derive(Debug, Clone)]
18pub struct NodeInfo {
19    pub node_id: String,
20    pub node_type: String,
21    pub host: String,
22    pub port: u16,
23    pub last_seen: SystemTime,
24    pub metadata: HashMap<String, String>,
25}
26
27/// Discovery configuration
28#[derive(Debug, Clone)]
29pub struct DiscoveryConfig {
30    pub redis_url: String,
31    pub discovery_topic: String,
32    pub heartbeat_interval: Duration,
33    pub node_timeout: Duration,
34    pub scan_subnet: Option<String>,
35}
36
37impl Default for DiscoveryConfig {
38    fn default() -> Self {
39        Self {
40            redis_url: "redis://localhost:6379".to_string(),
41            discovery_topic: "mecha10/discovery".to_string(),
42            heartbeat_interval: Duration::from_secs(5),
43            node_timeout: Duration::from_secs(30),
44            scan_subnet: None,
45        }
46    }
47}
48
49/// Discovery service for finding Mecha10 nodes
50///
51/// # Examples
52///
53/// ```rust,ignore
54/// use mecha10_cli::services::{DiscoveryService, DiscoveryConfig};
55///
56/// # async fn example() -> anyhow::Result<()> {
57/// let service = DiscoveryService::new();
58///
59/// // Configure discovery
60/// let config = DiscoveryConfig::default();
61///
62/// // Discover nodes
63/// let nodes = service.discover(&config).await?;
64/// for node in nodes {
65///     println!("Found node: {} at {}:{}", node.node_id, node.host, node.port);
66/// }
67///
68/// // Monitor for new nodes
69/// service.monitor(&config, |node| {
70///     println!("New node discovered: {}", node.node_id);
71/// }).await?;
72/// # Ok(())
73/// # }
74/// ```
75///
76/// This is a thin facade: every method here just forwards `discovered_nodes` (plus its own
77/// arguments) to a same-named function in `node_manager` or `redis_ops`, which hold the actual
78/// logic. cargo-dupes flags a couple of these forwarding pairs (e.g. `get_node`/
79/// `get_nodes_by_type`, `announce`/`heartbeat`) as exact duplicates since the wrapper shape
80/// (`pub fn X(&self, args) { module::X(&self.state, args) }`) is identical even though the
81/// wrapped functions do different things — that's inherent to the facade pattern, not
82/// duplicated business logic, so it's left as-is (LAB-2748).
83pub struct DiscoveryService {
84    discovered_nodes: HashMap<String, NodeInfo>,
85}
86
87impl DiscoveryService {
88    /// Create a new discovery service
89    pub fn new() -> Self {
90        Self {
91            discovered_nodes: HashMap::new(),
92        }
93    }
94
95    /// Discover active nodes on the network
96    ///
97    /// # Arguments
98    ///
99    /// * `config` - Discovery configuration
100    pub async fn discover(&mut self, config: &DiscoveryConfig) -> Result<Vec<NodeInfo>> {
101        redis_ops::discover(config, &mut self.discovered_nodes).await
102    }
103
104    /// Monitor for new nodes
105    ///
106    /// # Arguments
107    ///
108    /// * `config` - Discovery configuration
109    /// * `callback` - Function to call when a new node is discovered
110    pub async fn monitor<F>(&mut self, config: &DiscoveryConfig, callback: F) -> Result<()>
111    where
112        F: FnMut(&NodeInfo),
113    {
114        redis_ops::monitor(config, &mut self.discovered_nodes, callback).await
115    }
116
117    /// Get list of currently known nodes
118    pub fn get_nodes(&self) -> Vec<&NodeInfo> {
119        node_manager::get_nodes(&self.discovered_nodes)
120    }
121
122    /// Get a specific node by ID
123    ///
124    /// # Arguments
125    ///
126    /// * `node_id` - Node identifier
127    pub fn get_node(&self, node_id: &str) -> Option<&NodeInfo> {
128        node_manager::get_node(&self.discovered_nodes, node_id)
129    }
130
131    /// Remove stale nodes that haven't sent heartbeats
132    ///
133    /// # Arguments
134    ///
135    /// * `timeout` - Maximum time since last heartbeat
136    pub fn cleanup_stale_nodes(&mut self, timeout: Duration) -> usize {
137        node_manager::cleanup_stale_nodes(&mut self.discovered_nodes, timeout)
138    }
139
140    /// Broadcast presence announcement
141    ///
142    /// # Arguments
143    ///
144    /// * `config` - Discovery configuration
145    /// * `node_info` - Information about this node
146    pub async fn announce(&self, config: &DiscoveryConfig, node_info: &NodeInfo) -> Result<()> {
147        redis_ops::announce(config, node_info).await
148    }
149
150    /// Send heartbeat for a node
151    ///
152    /// # Arguments
153    ///
154    /// * `config` - Discovery configuration
155    /// * `node_id` - Node identifier
156    pub async fn heartbeat(&self, config: &DiscoveryConfig, node_id: &str) -> Result<()> {
157        redis_ops::heartbeat(config, node_id).await
158    }
159
160    /// Get node count
161    pub fn node_count(&self) -> usize {
162        node_manager::node_count(&self.discovered_nodes)
163    }
164
165    /// Get nodes by type
166    ///
167    /// # Arguments
168    ///
169    /// * `node_type` - Type of nodes to filter
170    pub fn get_nodes_by_type(&self, node_type: &str) -> Vec<&NodeInfo> {
171        node_manager::get_nodes_by_type(&self.discovered_nodes, node_type)
172    }
173
174    /// Clear all discovered nodes
175    pub fn clear(&mut self) {
176        node_manager::clear(&mut self.discovered_nodes);
177    }
178}
179
180impl Default for DiscoveryService {
181    fn default() -> Self {
182        Self::new()
183    }
184}