stackless_cloud/
health.rs1use std::time::Duration;
8
9const REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
11const POLL_INTERVAL: Duration = Duration::from_secs(5);
12
13#[derive(Debug, Clone)]
16pub struct HealthFailure {
17 pub url: String,
18 pub detail: String,
19 pub budget_secs: u64,
20}
21
22pub async fn poll(
26 url: &str,
27 expected_status: u16,
28 contains: Option<&str>,
29 budget: Duration,
30) -> Result<(), HealthFailure> {
31 let client = reqwest::Client::new();
32 let deadline = tokio::time::Instant::now() + budget;
33 let mut last_detail;
34 loop {
35 match client.get(url).timeout(REQUEST_TIMEOUT).send().await {
36 Ok(response) => {
37 let status = response.status().as_u16();
38 let body = response.text().await.unwrap_or_default();
39 let status_ok = status == expected_status;
40 let contains_ok = contains.is_none_or(|needle| body.contains(needle));
41 if status_ok && contains_ok {
42 return Ok(());
43 }
44 last_detail = format!(
45 "got {status}, expected {expected_status}{}",
46 contains
47 .map(|n| format!(" containing {n:?}"))
48 .unwrap_or_default()
49 );
50 }
51 Err(err) => last_detail = err.to_string(),
52 }
53 if tokio::time::Instant::now() >= deadline {
54 return Err(HealthFailure {
55 url: url.to_owned(),
56 detail: last_detail,
57 budget_secs: budget.as_secs(),
58 });
59 }
60 tokio::time::sleep(POLL_INTERVAL).await;
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use wiremock::matchers::{method, path};
68 use wiremock::{Mock, MockServer, ResponseTemplate};
69
70 #[tokio::test]
71 async fn green_on_status_and_body_match() {
72 let server = MockServer::start().await;
73 Mock::given(method("GET"))
74 .and(path("/health"))
75 .respond_with(ResponseTemplate::new(200).set_body_string("stackless-smoke-ok"))
76 .mount(&server)
77 .await;
78 let url = format!("{}/health", server.uri());
79 poll(
80 &url,
81 200,
82 Some("stackless-smoke-ok"),
83 Duration::from_secs(5),
84 )
85 .await
86 .unwrap();
87 }
88
89 #[tokio::test]
90 async fn times_out_when_status_never_matches() {
91 let server = MockServer::start().await;
92 Mock::given(method("GET"))
93 .and(path("/health"))
94 .respond_with(ResponseTemplate::new(503))
95 .mount(&server)
96 .await;
97 let url = format!("{}/health", server.uri());
98 let err = poll(&url, 200, None, Duration::from_secs(0))
100 .await
101 .unwrap_err();
102 assert_eq!(err.url, url);
103 assert_eq!(err.budget_secs, 0);
104 assert!(err.detail.contains("got 503"), "detail: {}", err.detail);
105 }
106}