readiness.rs 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. //! Engine readiness probes: poll `GET /v1/models` on the head node until
  2. //! the inference API answers — after `sm121 serve` launches containers
  3. //! (the model load can take many minutes) and before `sm121 bench` runs —
  4. //! plus a liveness watch for follow mode (the serve must fail loudly when
  5. //! the containers die mid-stream instead of tailing a dead engine).
  6. use crate::config::Cluster;
  7. use anyhow::Result;
  8. use std::time::{Duration, Instant};
  9. /// `sm121 serve`: big checkpoints load for many minutes.
  10. pub(crate) const SERVE_WAIT_SECS: u64 = 1800;
  11. /// `sm121 bench`: the serve is expected to already be up.
  12. pub(crate) const BENCH_WAIT_SECS: u64 = 900;
  13. /// Liveness watch: probe interval / consecutive misses that mean "died".
  14. const WATCH_POLL_SECS: u64 = 30;
  15. const WATCH_MISSES: u32 = 2;
  16. fn head_url(cluster: &Cluster, port: u16) -> String {
  17. let head = cluster
  18. .nodes
  19. .iter()
  20. .find(|n| n.rank == 0)
  21. .or_else(|| cluster.nodes.first());
  22. format!(
  23. "http://{}:{}/v1",
  24. head.map_or("", |n| n.ssh_ip.as_str()),
  25. port
  26. )
  27. }
  28. /// Poll `GET <base>/models` until it answers (engine up) or the timeout
  29. /// hits; progress note every 30 s.
  30. pub(crate) async fn wait_ready(base_url: &str, timeout_secs: u64) -> Result<()> {
  31. let client = reqwest::Client::builder()
  32. .timeout(Duration::from_secs(10))
  33. .build()?;
  34. let deadline = Instant::now() + Duration::from_secs(timeout_secs);
  35. let mut since_note = Instant::now();
  36. loop {
  37. if let Ok(r) = client.get(format!("{base_url}/models")).send().await
  38. && r.status().is_success()
  39. {
  40. return Ok(());
  41. }
  42. if since_note.elapsed() >= Duration::from_secs(30) {
  43. since_note = Instant::now();
  44. eprintln!("waiting for API at {base_url} (up to {timeout_secs} s)...");
  45. }
  46. if Instant::now() > deadline {
  47. anyhow::bail!("API at {base_url} not ready after {timeout_secs} s");
  48. }
  49. tokio::time::sleep(Duration::from_secs(5)).await;
  50. }
  51. }
  52. /// Serve-side readiness: the head node's engine API must answer.
  53. pub(crate) async fn wait_engine(cluster: &Cluster, port: u16, timeout_secs: u64) -> Result<()> {
  54. wait_ready(&head_url(cluster, port), timeout_secs).await
  55. }
  56. /// One-shot liveness: is the engine answering right now?
  57. pub(crate) async fn alive(cluster: &Cluster, port: u16) -> bool {
  58. let client = reqwest::Client::builder()
  59. .timeout(Duration::from_secs(10))
  60. .build()
  61. .ok();
  62. let Some(client) = client else {
  63. return false;
  64. };
  65. client
  66. .get(format!("{}/models", head_url(cluster, port)))
  67. .send()
  68. .await
  69. .is_ok_and(|r| r.status().is_success())
  70. }
  71. /// Follow-mode liveness watch: resolves `Err` once the engine misses
  72. /// `WATCH_MISSES` consecutive probes — the containers died mid-follow.
  73. pub(crate) async fn watch(cluster: &Cluster, port: u16) -> Result<()> {
  74. let mut misses = 0;
  75. loop {
  76. tokio::time::sleep(Duration::from_secs(WATCH_POLL_SECS)).await;
  77. if alive(cluster, port).await {
  78. misses = 0;
  79. } else {
  80. misses += 1;
  81. if misses >= WATCH_MISSES {
  82. anyhow::bail!(
  83. "engine stopped answering at {} — containers died mid-follow",
  84. head_url(cluster, port)
  85. );
  86. }
  87. }
  88. }
  89. }