stop.rs 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. //! Builder for the `sm121 stop` per-node remote command.
  2. //!
  3. //! `docker stop` only returns once containers reach the `Exited` state; the
  4. //! `--rm` removal and the GPU/CUDA-context teardown can lag behind it.
  5. //! Re-serving on the same node in that window makes the new container race
  6. //! the dying one (immediate worker death). By default the command therefore
  7. //! verifies — polls up to 60 s — that no labeled container is running before
  8. //! reporting success; `--no-wait` restores the legacy fire-and-forget behavior.
  9. use super::shq;
  10. /// Builds the per-node remote stop command.
  11. ///
  12. /// `wait = true` (default): after `docker stop`, poll every 2 s (30 times,
  13. /// 60 s max) until `docker ps` shows no container with the `sm121.recipe` /
  14. /// `sm121.cluster` labels in the running state; if any remain, exit 1 —
  15. /// `run_ssh` surfaces that as an error.
  16. pub fn remote_command(recipe_name: &str, cluster_name: &str, rank: u32, wait: bool) -> String {
  17. let filter = format!(
  18. "--filter {} --filter {}",
  19. shq(&format!("label=sm121.recipe={}", recipe_name)),
  20. shq(&format!("label=sm121.cluster={}", cluster_name)),
  21. );
  22. let wait_block = if wait {
  23. "for _ in $(seq 1 30); do \
  24. n=$(docker ps -q {filter}); \
  25. [ -z \"$n\" ] && break; \
  26. sleep 2; \
  27. done; \
  28. n=$(docker ps -q {filter}); \
  29. if [ -n \"$n\" ]; then \
  30. echo \"sm121 stop: containers still running after 60 s: $n\" >&2; \
  31. exit 1; \
  32. fi; "
  33. .to_string()
  34. } else {
  35. String::new()
  36. };
  37. format!(
  38. "docker ps -q {filter} | xargs -r docker stop; rm -rf {tmp} 2>/dev/null; {wait_block}true",
  39. tmp = shq(&format!("/tmp/sm121_{}_{}", recipe_name, rank)),
  40. wait_block = wait_block,
  41. )
  42. }
  43. /// Handle the `stop` command: stop containers and optionally power off.
  44. ///
  45. /// Waits for the containers to actually stop (see [`remote_command`]), then
  46. /// runs the recipe's `after_stop` hooks and the optional power off.
  47. pub async fn handle(
  48. config: &crate::config::Config,
  49. recipe_name: &str,
  50. cluster_name: &str,
  51. nodes: &[String],
  52. wait: bool,
  53. power_off: bool,
  54. dry_run: bool,
  55. ) -> Result<(), anyhow::Error> {
  56. let (cluster, nodes): (crate::config::Cluster, Vec<String>) = if !nodes.is_empty() {
  57. let cluster = crate::resolve_cluster_for_op(config, recipe_name, cluster_name).await?;
  58. (cluster, nodes.to_vec())
  59. } else {
  60. let variants =
  61. crate::docker::deploy::discover_recipe_deployments(config, recipe_name).await?;
  62. if variants.is_empty() {
  63. anyhow::bail!("No running containers found for recipe '{}'", recipe_name);
  64. }
  65. if variants.len() == 1 {
  66. let v = &variants[0];
  67. let cluster = crate::config::select_cluster_by_name(config, &v.cluster)?;
  68. (cluster, v.nodes.clone())
  69. } else {
  70. anyhow::bail!(crate::docker::deploy::multi_deployment_error(
  71. recipe_name,
  72. &variants
  73. ));
  74. }
  75. };
  76. crate::docker::stop_cluster(config, &cluster, recipe_name, &nodes, wait, dry_run).await?;
  77. // Run recipe `after_stop` hooks (e.g. restore_dgx_services) on the stopped nodes; best-effort.
  78. if let Ok(recipe) = crate::config::load_recipe(recipe_name, &config.recipe_sources).await {
  79. crate::after_stop::run(config, &cluster, &recipe, dry_run).await?;
  80. }
  81. if power_off {
  82. crate::power::power_off(config, &cluster, &nodes, dry_run).await?;
  83. }
  84. Ok(())
  85. }
  86. #[cfg(test)]
  87. mod tests {
  88. use super::remote_command;
  89. #[test]
  90. fn default_includes_verification_loop() {
  91. let cmd = remote_command("r", "c", 0, true);
  92. assert!(cmd.contains("xargs -r docker stop"));
  93. assert!(cmd.contains("seq 1 30"));
  94. assert!(cmd.contains("exit 1"));
  95. assert!(cmd.ends_with("true"));
  96. }
  97. #[test]
  98. fn no_wait_omits_verification_loop() {
  99. let cmd = remote_command("r", "c", 1, false);
  100. assert!(!cmd.contains("seq 1 30"));
  101. assert!(!cmd.contains("exit 1"));
  102. assert!(cmd.contains("/tmp/sm121_r_1"));
  103. }
  104. }