@jpgilldev / steid

2.4 KBRaw
1//! Liveness for a process supervisor and an uptime check.
2
3use sqlx::SqlitePool;
4use topcoat::{
5 Result,
6 context::Cx,
7 router::{StatusCode, route},
8};
9
10use super::context::pool;
11
12/// Whether this instance can serve.
13///
14/// Unauthenticated by design — a health check that needs a session cannot be polled
15/// by the thing that restarts the process — so the body is two fixed words and says
16/// nothing about the instance: not its version, not its name, not whether it is
17/// claimed.
18///
19/// It touches the database because the pool is what an "up" process most plausibly
20/// loses: the file moved, the disk filled, the connection limit exhausted. A check
21/// that cannot fail is decoration. `SELECT 1` costs a round trip to a local file and
22/// no table access, which is cheap enough to poll every few seconds.
23#[route(GET "/healthz")]
24async fn healthz(cx: &Cx) -> Result<(StatusCode, &'static str)> {
25 Ok(match reachable(pool(cx)).await {
26 Ok(()) => (StatusCode::OK, "ok\n"),
27 Err(error) => {
28 // The reason goes to the log, where an operator can see it; the response
29 // stays opaque because anyone can reach this.
30 eprintln!("steid: health check failed: {error}");
31 (StatusCode::SERVICE_UNAVAILABLE, "unavailable\n")
32 }
33 })
34}
35
36async fn reachable(pool: &SqlitePool) -> Result<(), sqlx::Error> {
37 sqlx::query("SELECT 1").fetch_one(pool).await.map(|_| ())
38}
39
40#[cfg(test)]
41mod tests {
42 use std::str::FromStr;
43
44 use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
45
46 use super::*;
47 use crate::infrastructure::database::test_support::test_pool;
48
49 #[tokio::test]
50 async fn a_working_pool_is_reachable() {
51 let pool = test_pool().await;
52
53 assert!(reachable(&pool).await.is_ok());
54 }
55
56 #[tokio::test]
57 async fn a_broken_pool_is_not_reachable() {
58 // Lazy so the failure lands on the query rather than on construction, which
59 // is how the interesting case arrives in production: a pool that opened fine
60 // at boot and cannot reach its file now.
61 let options = SqliteConnectOptions::from_str("sqlite:/nonexistent/steid.db")
62 .expect("the url should parse");
63 let pool = SqlitePoolOptions::new().connect_lazy_with(options);
64
65 assert!(
66 reachable(&pool).await.is_err(),
67 "a database that cannot be opened must fail the check"
68 );
69 }
70}