health.rs 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  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. (node.name.clone(), node.ib_ip.clone(), node.rank, health)
  62. }));
  63. }
  64. let results = futures::future::join_all(handles).await;
  65. for result in results {
  66. match result {
  67. Ok((name, ib_ip, rank, Ok(health))) => {
  68. let status = classify_status(&health);
  69. nodes.push(NodeHealth {
  70. name,
  71. ib_ip,
  72. rank,
  73. session_exists: health.session_exists,
  74. process_running: health.process_alive,
  75. port_accessible: health.api_ok,
  76. status,
  77. });
  78. }
  79. Ok((name, ib_ip, rank, Err(_))) => {
  80. nodes.push(NodeHealth {
  81. name,
  82. ib_ip,
  83. rank,
  84. session_exists: false,
  85. process_running: false,
  86. port_accessible: false,
  87. status: SessionStatus::Unknown,
  88. });
  89. }
  90. Err(e) => {
  91. // A health-check task panicked (JoinError) — log it so a node
  92. // silently reported "Unknown" is diagnosable.
  93. warn!("Health-check task panicked: {}", e);
  94. }
  95. }
  96. }
  97. let overall = compute_overall_status(&nodes);
  98. Ok(HealthReport { nodes, overall })
  99. }
  100. /// Classifies overall session status from detailed health data.
  101. pub fn classify_status(health: &SessionHealth) -> SessionStatus {
  102. if health.is_healthy() {
  103. SessionStatus::Running
  104. } else if health.is_degraded() || (health.session_exists && !health.gpu_ok) {
  105. SessionStatus::Degraded
  106. } else {
  107. SessionStatus::Stopped
  108. }
  109. }
  110. /// Computes overall cluster health from per-node statuses.
  111. fn compute_overall_status(nodes: &[NodeHealth]) -> HealthStatus {
  112. let has_unknown = nodes.iter().any(|n| n.status == SessionStatus::Unknown);
  113. let has_stopped = nodes.iter().any(|n| n.status == SessionStatus::Stopped);
  114. let has_degraded = nodes.iter().any(|n| n.status == SessionStatus::Degraded);
  115. let all_running = nodes.iter().all(|n| n.status == SessionStatus::Running);
  116. if all_running {
  117. HealthStatus::Healthy
  118. } else if !has_unknown && !has_stopped && has_degraded {
  119. HealthStatus::Degraded
  120. } else {
  121. HealthStatus::Unhealthy
  122. }
  123. }