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}