health.rs 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. use anyhow::Result;
  2. use tracing::warn;
  3. use crate::config::{Cluster, Config, Recipe};
  4. use crate::tmux::{SessionHealth, TmuxManager};
  5. /// Health status of a node's tmux session
  6. #[derive(Debug, Clone, PartialEq)]
  7. pub enum SessionStatus {
  8. Running,
  9. Stopped,
  10. Degraded,
  11. Unknown,
  12. }
  13. /// Health report for a single node
  14. #[derive(Debug, Clone)]
  15. pub struct NodeHealth {
  16. pub name: String,
  17. pub ib_ip: String,
  18. pub rank: u32,
  19. pub session_exists: bool,
  20. pub process_running: bool,
  21. pub port_accessible: bool,
  22. pub status: SessionStatus,
  23. }
  24. /// Overall health report for a cluster
  25. #[derive(Debug, Clone)]
  26. pub struct HealthReport {
  27. pub nodes: Vec<NodeHealth>,
  28. pub overall: HealthStatus,
  29. }
  30. /// Overall cluster health status
  31. #[derive(Debug, Clone, PartialEq)]
  32. pub enum HealthStatus {
  33. Healthy,
  34. Degraded,
  35. Unhealthy,
  36. }
  37. /// Checks the health of all tmux sessions for a recipe across a cluster.
  38. /// Uses deep health checks: session existence, process liveness, GPU status, API reachability.
  39. /// Returns a [`HealthReport`] with per-node details — no output is printed by this function.
  40. pub async fn check_cluster_health(
  41. manager: &TmuxManager,
  42. config: &Config,
  43. cluster: &Cluster,
  44. recipe: &Recipe,
  45. dry_run: bool,
  46. ) -> Result<HealthReport> {
  47. let mut nodes: Vec<NodeHealth> = Vec::new();
  48. let mut handles = Vec::new();
  49. for node in &cluster.nodes {
  50. let sess = manager.session_name(recipe, node.rank);
  51. let config = config.clone();
  52. let node = node.clone();
  53. let cluster = cluster.clone();
  54. let recipe = recipe.clone();
  55. let dry = dry_run;
  56. let manager = manager.clone();
  57. handles.push(crate::locale::spawn_with_locale(async move {
  58. let health = manager
  59. .deep_check_session(&config, &node, &cluster, &sess, &recipe, dry)
  60. .await;
  61. (
  62. node.name.clone(),
  63. node.effective_ib_ip().to_string(),
  64. node.rank,
  65. health,
  66. )
  67. }));
  68. }
  69. let results = futures::future::join_all(handles).await;
  70. for result in results {
  71. match result {
  72. Ok((name, ib_ip, rank, Ok(health))) => {
  73. let status = classify_status(&health);
  74. nodes.push(NodeHealth {
  75. name,
  76. ib_ip,
  77. rank,
  78. session_exists: health.session_exists,
  79. process_running: health.process_alive,
  80. port_accessible: health.api_ok,
  81. status,
  82. });
  83. }
  84. Ok((name, ib_ip, rank, Err(_))) => {
  85. nodes.push(NodeHealth {
  86. name,
  87. ib_ip,
  88. rank,
  89. session_exists: false,
  90. process_running: false,
  91. port_accessible: false,
  92. status: SessionStatus::Unknown,
  93. });
  94. }
  95. Err(e) => {
  96. // A health-check task panicked (JoinError) — log it so a node
  97. // silently reported "Unknown" is diagnosable.
  98. warn!("Health-check task panicked: {}", e);
  99. }
  100. }
  101. }
  102. let overall = compute_overall_status(&nodes);
  103. Ok(HealthReport { nodes, overall })
  104. }
  105. /// Classifies overall session status from detailed health data.
  106. pub fn classify_status(health: &SessionHealth) -> SessionStatus {
  107. if health.is_healthy() {
  108. SessionStatus::Running
  109. } else if health.is_degraded() || (health.session_exists && !health.gpu_ok) {
  110. SessionStatus::Degraded
  111. } else {
  112. SessionStatus::Stopped
  113. }
  114. }
  115. /// Computes overall cluster health from per-node statuses.
  116. fn compute_overall_status(nodes: &[NodeHealth]) -> HealthStatus {
  117. let has_unknown = nodes.iter().any(|n| n.status == SessionStatus::Unknown);
  118. let has_stopped = nodes.iter().any(|n| n.status == SessionStatus::Stopped);
  119. let has_degraded = nodes.iter().any(|n| n.status == SessionStatus::Degraded);
  120. let all_running = nodes.iter().all(|n| n.status == SessionStatus::Running);
  121. if all_running {
  122. HealthStatus::Healthy
  123. } else if !has_unknown && !has_stopped && has_degraded {
  124. HealthStatus::Degraded
  125. } else {
  126. HealthStatus::Unhealthy
  127. }
  128. }