use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use async_trait::async_trait;
use tracing::info;
use crate::health::HealthStatus;
use crate::mesh_runtime::MeshRuntime;
use crate::service::{ServiceType, SystemService};
pub struct MeshService {
runtime: Arc<MeshRuntime>,
started: AtomicBool,
listen_addr: Option<String>,
transport: Option<String>,
tasks: std::sync::Mutex<Vec<tokio::task::JoinHandle<()>>>,
}
impl Drop for MeshService {
fn drop(&mut self) {
self.abort_tasks();
}
}
impl MeshService {
pub fn adopt_tasks(&self, handles: Vec<tokio::task::JoinHandle<()>>) {
self.tasks.lock().unwrap().extend(handles);
}
fn abort_tasks(&self) {
for h in self.tasks.lock().unwrap().drain(..) {
h.abort();
}
}
pub fn new(runtime: Arc<MeshRuntime>) -> Self {
Self {
runtime,
started: AtomicBool::new(false),
listen_addr: None,
transport: None,
tasks: Default::default(),
}
}
pub fn with_endpoints(
runtime: Arc<MeshRuntime>,
listen_addr: impl Into<String>,
transport: impl Into<String>,
) -> Self {
Self {
runtime,
started: AtomicBool::new(false),
listen_addr: Some(listen_addr.into()),
transport: Some(transport.into()),
tasks: Default::default(),
}
}
pub fn runtime(&self) -> &Arc<MeshRuntime> {
&self.runtime
}
pub fn is_started(&self) -> bool {
self.started.load(Ordering::SeqCst)
}
pub fn listen_addr(&self) -> Option<&str> {
self.listen_addr.as_deref()
}
pub fn transport_name(&self) -> Option<&str> {
self.transport.as_deref()
}
}
#[async_trait]
impl SystemService for MeshService {
fn name(&self) -> &str {
"mesh"
}
fn service_type(&self) -> ServiceType {
ServiceType::Core
}
async fn start(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let was = self.started.swap(true, Ordering::SeqCst);
if was {
info!(
node = %self.runtime.node_id(),
peers = self.runtime.peer_count(),
"mesh service start (already running)"
);
return Ok(());
}
info!(
node = %self.runtime.node_id(),
peers = self.runtime.peer_count(),
listen = self.listen_addr.as_deref().unwrap_or("-"),
transport = self.transport.as_deref().unwrap_or("-"),
"mesh service started"
);
Ok(())
}
async fn stop(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let peer_count = self.runtime.peer_count();
self.abort_tasks();
self.runtime.disconnect_all_peers();
self.started.store(false, Ordering::SeqCst);
info!(
node = %self.runtime.node_id(),
peers_disconnected = peer_count,
"mesh service stopped"
);
Ok(())
}
async fn health_check(&self) -> HealthStatus {
if !self.started.load(Ordering::SeqCst) {
return HealthStatus::Unhealthy("mesh service not started".into());
}
let unhealthy = self.runtime.check_peer_health();
let peers = self.runtime.peer_count();
if unhealthy.is_empty() {
HealthStatus::Healthy
} else if peers == 0 || unhealthy.len() >= peers {
HealthStatus::Unhealthy(format!(
"all tracked peers unhealthy: {}",
unhealthy.join(", ")
))
} else {
HealthStatus::Degraded(format!(
"{}/{} peers unhealthy: {}",
unhealthy.len(),
peers,
unhealthy.join(", ")
))
}
}
#[cfg(feature = "os-patterns")]
async fn liveness_check(&self) -> crate::health::ProbeResult {
if self.started.load(Ordering::SeqCst) {
crate::health::ProbeResult::Live
} else {
crate::health::ProbeResult::NotLive {
reason: "mesh not started".into(),
}
}
}
#[cfg(feature = "os-patterns")]
async fn readiness_check(&self) -> crate::health::ProbeResult {
if self.started.load(Ordering::SeqCst) {
crate::health::ProbeResult::Ready
} else {
crate::health::ProbeResult::NotReady {
reason: "mesh not started".into(),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::service::ServiceRegistry;
#[tokio::test]
async fn lifecycle_start_stop_health() {
let rt = Arc::new(MeshRuntime::new("node-test".into()));
let svc = MeshService::with_endpoints(Arc::clone(&rt), "127.0.0.1:9489", "tcp");
assert!(!svc.is_started());
assert!(matches!(
svc.health_check().await,
HealthStatus::Unhealthy(_)
));
svc.start().await.expect("start");
assert!(svc.is_started());
assert_eq!(svc.health_check().await, HealthStatus::Healthy);
svc.start().await.expect("start again");
assert!(svc.is_started());
let (tx, _rx) = tokio::sync::mpsc::channel(4);
rt.add_peer("peer-a".into(), tx);
assert_eq!(rt.peer_count(), 1);
svc.stop().await.expect("stop");
assert!(!svc.is_started());
assert_eq!(rt.peer_count(), 0, "stop disconnects all peers");
assert!(matches!(
svc.health_check().await,
HealthStatus::Unhealthy(_)
));
}
#[tokio::test]
async fn registry_start_all_stop_all() {
let rt = Arc::new(MeshRuntime::new("node-reg".into()));
let svc = Arc::new(MeshService::new(Arc::clone(&rt)));
let registry = ServiceRegistry::new();
registry.register(svc.clone()).expect("register");
assert!(registry.get("mesh").is_some());
assert_eq!(svc.name(), "mesh");
assert_eq!(svc.service_type(), ServiceType::Core);
registry.start_all().await.expect("start_all");
assert!(svc.is_started());
let health = registry.health_all().await;
let mesh_health = health
.iter()
.find(|(n, _)| n == "mesh")
.map(|(_, h)| h.clone())
.expect("mesh in health_all");
assert_eq!(mesh_health, HealthStatus::Healthy);
let (tx, _rx) = tokio::sync::mpsc::channel(4);
rt.add_peer("p1".into(), tx);
assert_eq!(rt.peer_count(), 1);
registry.stop_all().await.expect("stop_all");
assert!(!svc.is_started());
assert_eq!(rt.peer_count(), 0);
}
#[tokio::test]
async fn health_unhealthy_when_all_discovery_peers_dead() {
use crate::mesh_heartbeat::HeartbeatConfig;
use std::time::Duration;
let mut rt = MeshRuntime::with_discovery("node-d".into(), [1u8; 32]);
{
let disc = rt.discovery_mut().expect("discovery");
*disc.heartbeat.lock().unwrap() =
crate::mesh_heartbeat::HeartbeatTracker::new(HeartbeatConfig {
suspect_timeout: Duration::from_secs(0),
..HeartbeatConfig::default()
});
let mut hb = disc.heartbeat.lock().unwrap();
hb.add_peer("dying".into());
hb.record_miss("dying");
hb.record_miss("dying");
assert_eq!(hb.dead_peers().len(), 1);
}
let rt = Arc::new(rt);
let (tx, _rx) = tokio::sync::mpsc::channel(4);
rt.add_peer("dying".into(), tx);
let svc = MeshService::new(Arc::clone(&rt));
svc.start().await.unwrap();
let status = svc.health_check().await;
assert!(
matches!(status, HealthStatus::Unhealthy(_)),
"expected Unhealthy when sole peer is dead, got {status}"
);
}
#[tokio::test]
async fn stop_ends_the_dial_and_accept_loops() {
let rt = Arc::new(MeshRuntime::new("n".into()));
let svc = MeshService::new(Arc::clone(&rt));
let handles = crate::mesh_serve::connect_seeds(
&rt,
&["127.0.0.1:1".to_string()],
"tcp",
None,
None,
);
let aborts: Vec<_> = handles.iter().map(|h| h.abort_handle()).collect();
svc.adopt_tasks(handles);
svc.start().await.unwrap();
assert!(aborts.iter().all(|a| !a.is_finished()));
svc.stop().await.unwrap();
for _ in 0..2000 {
if aborts.iter().all(|a| a.is_finished()) {
return;
}
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
panic!("dial loops still running after stop");
}
#[test]
fn endpoints_accessors() {
let rt = Arc::new(MeshRuntime::new("n".into()));
let svc = MeshService::with_endpoints(Arc::clone(&rt), "0.0.0.0:9489", "ws");
assert_eq!(svc.listen_addr(), Some("0.0.0.0:9489"));
assert_eq!(svc.transport_name(), Some("ws"));
assert_eq!(svc.runtime().node_id(), "n");
}
}