clawft_kernel/
mesh_system_service.rs1use std::sync::Arc;
10use std::sync::atomic::{AtomicBool, Ordering};
11
12use async_trait::async_trait;
13use tracing::info;
14
15use crate::health::HealthStatus;
16use crate::mesh_runtime::MeshRuntime;
17use crate::service::{ServiceType, SystemService};
18
19pub struct MeshService {
27 runtime: Arc<MeshRuntime>,
28 started: AtomicBool,
29 listen_addr: Option<String>,
31 transport: Option<String>,
33 tasks: std::sync::Mutex<Vec<tokio::task::JoinHandle<()>>>,
36}
37
38impl Drop for MeshService {
39 fn drop(&mut self) {
40 self.abort_tasks();
41 }
42}
43
44impl MeshService {
45 pub fn adopt_tasks(&self, handles: Vec<tokio::task::JoinHandle<()>>) {
48 self.tasks.lock().unwrap().extend(handles);
49 }
50
51 fn abort_tasks(&self) {
52 for h in self.tasks.lock().unwrap().drain(..) {
53 h.abort();
54 }
55 }
56
57 pub fn new(runtime: Arc<MeshRuntime>) -> Self {
59 Self {
60 runtime,
61 started: AtomicBool::new(false),
62 listen_addr: None,
63 transport: None,
64 tasks: Default::default(),
65 }
66 }
67
68 pub fn with_endpoints(
70 runtime: Arc<MeshRuntime>,
71 listen_addr: impl Into<String>,
72 transport: impl Into<String>,
73 ) -> Self {
74 Self {
75 runtime,
76 started: AtomicBool::new(false),
77 listen_addr: Some(listen_addr.into()),
78 transport: Some(transport.into()),
79 tasks: Default::default(),
80 }
81 }
82
83 pub fn runtime(&self) -> &Arc<MeshRuntime> {
85 &self.runtime
86 }
87
88 pub fn is_started(&self) -> bool {
91 self.started.load(Ordering::SeqCst)
92 }
93
94 pub fn listen_addr(&self) -> Option<&str> {
96 self.listen_addr.as_deref()
97 }
98
99 pub fn transport_name(&self) -> Option<&str> {
101 self.transport.as_deref()
102 }
103}
104
105#[async_trait]
106impl SystemService for MeshService {
107 fn name(&self) -> &str {
108 "mesh"
109 }
110
111 fn service_type(&self) -> ServiceType {
112 ServiceType::Core
113 }
114
115 async fn start(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
116 let was = self.started.swap(true, Ordering::SeqCst);
119 if was {
120 info!(
121 node = %self.runtime.node_id(),
122 peers = self.runtime.peer_count(),
123 "mesh service start (already running)"
124 );
125 return Ok(());
126 }
127 info!(
128 node = %self.runtime.node_id(),
129 peers = self.runtime.peer_count(),
130 listen = self.listen_addr.as_deref().unwrap_or("-"),
131 transport = self.transport.as_deref().unwrap_or("-"),
132 "mesh service started"
133 );
134 Ok(())
135 }
136
137 async fn stop(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
138 let peer_count = self.runtime.peer_count();
139 self.abort_tasks();
142 self.runtime.disconnect_all_peers();
143 self.started.store(false, Ordering::SeqCst);
144 info!(
145 node = %self.runtime.node_id(),
146 peers_disconnected = peer_count,
147 "mesh service stopped"
148 );
149 Ok(())
150 }
151
152 async fn health_check(&self) -> HealthStatus {
153 if !self.started.load(Ordering::SeqCst) {
154 return HealthStatus::Unhealthy("mesh service not started".into());
155 }
156
157 let unhealthy = self.runtime.check_peer_health();
158 let peers = self.runtime.peer_count();
159 if unhealthy.is_empty() {
160 HealthStatus::Healthy
161 } else if peers == 0 || unhealthy.len() >= peers {
162 HealthStatus::Unhealthy(format!(
163 "all tracked peers unhealthy: {}",
164 unhealthy.join(", ")
165 ))
166 } else {
167 HealthStatus::Degraded(format!(
168 "{}/{} peers unhealthy: {}",
169 unhealthy.len(),
170 peers,
171 unhealthy.join(", ")
172 ))
173 }
174 }
175
176 #[cfg(feature = "os-patterns")]
177 async fn liveness_check(&self) -> crate::health::ProbeResult {
178 if self.started.load(Ordering::SeqCst) {
179 crate::health::ProbeResult::Live
180 } else {
181 crate::health::ProbeResult::NotLive {
182 reason: "mesh not started".into(),
183 }
184 }
185 }
186
187 #[cfg(feature = "os-patterns")]
188 async fn readiness_check(&self) -> crate::health::ProbeResult {
189 if self.started.load(Ordering::SeqCst) {
190 crate::health::ProbeResult::Ready
191 } else {
192 crate::health::ProbeResult::NotReady {
193 reason: "mesh not started".into(),
194 }
195 }
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use crate::service::ServiceRegistry;
203
204 #[tokio::test]
205 async fn lifecycle_start_stop_health() {
206 let rt = Arc::new(MeshRuntime::new("node-test".into()));
207 let svc = MeshService::with_endpoints(Arc::clone(&rt), "127.0.0.1:9489", "tcp");
208
209 assert!(!svc.is_started());
210 assert!(matches!(
211 svc.health_check().await,
212 HealthStatus::Unhealthy(_)
213 ));
214
215 svc.start().await.expect("start");
216 assert!(svc.is_started());
217 assert_eq!(svc.health_check().await, HealthStatus::Healthy);
218
219 svc.start().await.expect("start again");
221 assert!(svc.is_started());
222
223 let (tx, _rx) = tokio::sync::mpsc::channel(4);
224 rt.add_peer("peer-a".into(), tx);
225 assert_eq!(rt.peer_count(), 1);
226
227 svc.stop().await.expect("stop");
228 assert!(!svc.is_started());
229 assert_eq!(rt.peer_count(), 0, "stop disconnects all peers");
230 assert!(matches!(
231 svc.health_check().await,
232 HealthStatus::Unhealthy(_)
233 ));
234 }
235
236 #[tokio::test]
237 async fn registry_start_all_stop_all() {
238 let rt = Arc::new(MeshRuntime::new("node-reg".into()));
239 let svc = Arc::new(MeshService::new(Arc::clone(&rt)));
240 let registry = ServiceRegistry::new();
241 registry.register(svc.clone()).expect("register");
242
243 assert!(registry.get("mesh").is_some());
244 assert_eq!(svc.name(), "mesh");
245 assert_eq!(svc.service_type(), ServiceType::Core);
246
247 registry.start_all().await.expect("start_all");
248 assert!(svc.is_started());
249 let health = registry.health_all().await;
250 let mesh_health = health
251 .iter()
252 .find(|(n, _)| n == "mesh")
253 .map(|(_, h)| h.clone())
254 .expect("mesh in health_all");
255 assert_eq!(mesh_health, HealthStatus::Healthy);
256
257 let (tx, _rx) = tokio::sync::mpsc::channel(4);
258 rt.add_peer("p1".into(), tx);
259 assert_eq!(rt.peer_count(), 1);
260
261 registry.stop_all().await.expect("stop_all");
262 assert!(!svc.is_started());
263 assert_eq!(rt.peer_count(), 0);
264 }
265
266 #[tokio::test]
267 async fn health_unhealthy_when_all_discovery_peers_dead() {
268 use crate::mesh_heartbeat::HeartbeatConfig;
269 use std::time::Duration;
270
271 let mut rt = MeshRuntime::with_discovery("node-d".into(), [1u8; 32]);
272 {
273 let disc = rt.discovery_mut().expect("discovery");
274 *disc.heartbeat.lock().unwrap() =
276 crate::mesh_heartbeat::HeartbeatTracker::new(HeartbeatConfig {
277 suspect_timeout: Duration::from_secs(0),
278 ..HeartbeatConfig::default()
279 });
280 let mut hb = disc.heartbeat.lock().unwrap();
281 hb.add_peer("dying".into());
282 hb.record_miss("dying");
284 hb.record_miss("dying");
285 assert_eq!(hb.dead_peers().len(), 1);
286 }
287 let rt = Arc::new(rt);
288 let (tx, _rx) = tokio::sync::mpsc::channel(4);
290 rt.add_peer("dying".into(), tx);
291
292 let svc = MeshService::new(Arc::clone(&rt));
293 svc.start().await.unwrap();
294 let status = svc.health_check().await;
295 assert!(
296 matches!(status, HealthStatus::Unhealthy(_)),
297 "expected Unhealthy when sole peer is dead, got {status}"
298 );
299 }
300
301 #[tokio::test]
302 async fn stop_ends_the_dial_and_accept_loops() {
303 let rt = Arc::new(MeshRuntime::new("n".into()));
304 let svc = MeshService::new(Arc::clone(&rt));
305 let handles = crate::mesh_serve::connect_seeds(
307 &rt,
308 &["127.0.0.1:1".to_string()],
309 "tcp",
310 None,
311 None,
312 );
313 let aborts: Vec<_> = handles.iter().map(|h| h.abort_handle()).collect();
314 svc.adopt_tasks(handles);
315 svc.start().await.unwrap();
316 assert!(aborts.iter().all(|a| !a.is_finished()));
317 svc.stop().await.unwrap();
318 for _ in 0..2000 {
319 if aborts.iter().all(|a| a.is_finished()) {
320 return;
321 }
322 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
323 }
324 panic!("dial loops still running after stop");
325 }
326
327 #[test]
328 fn endpoints_accessors() {
329 let rt = Arc::new(MeshRuntime::new("n".into()));
330 let svc = MeshService::with_endpoints(Arc::clone(&rt), "0.0.0.0:9489", "ws");
331 assert_eq!(svc.listen_addr(), Some("0.0.0.0:9489"));
332 assert_eq!(svc.transport_name(), Some("ws"));
333 assert_eq!(svc.runtime().node_id(), "n");
334 }
335}