health.rs 3.9 KB

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