95 lines
3.1 KiB
Rust
95 lines
3.1 KiB
Rust
use std::io::{Read, Write};
|
|
use std::net::{SocketAddr, TcpStream};
|
|
use std::time::Duration;
|
|
use actix_web::{get, HttpResponse, Responder};
|
|
use actix_web::web::Data;
|
|
use log::error;
|
|
use crate::backend::AppData;
|
|
|
|
/// Health of the instance for the reverse proxy and for monitoring. The application is
|
|
/// healthy when it can talk to its database.
|
|
#[get("/health")]
|
|
pub async fn health(app_data: Data<AppData>) -> impl Responder {
|
|
match sqlx::query("SELECT 1").execute(app_data.db_pool()).await {
|
|
Ok(_) => HttpResponse::Ok().body("ok"),
|
|
Err(e) => {
|
|
error!("Health check failed: {}", e);
|
|
HttpResponse::ServiceUnavailable().body("database unavailable")
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Asks a running instance on this machine for its health. Used by the container health
|
|
/// check (`rezervator --healthcheck`), so the image doesn't need curl.
|
|
pub fn is_healthy(port: u16) -> bool {
|
|
let timeout = Duration::from_secs(3);
|
|
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
|
|
|
let Ok(mut stream) = TcpStream::connect_timeout(&addr, timeout) else {
|
|
return false
|
|
};
|
|
let _ = stream.set_read_timeout(Some(timeout));
|
|
let _ = stream.set_write_timeout(Some(timeout));
|
|
|
|
if stream.write_all(b"GET /health HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n").is_err() {
|
|
return false
|
|
}
|
|
|
|
let mut response = [0u8; 64];
|
|
let read = stream.read(&mut response).unwrap_or(0);
|
|
|
|
String::from_utf8_lossy(&response[..read]).lines().next()
|
|
.map(|status| {
|
|
let mut parts = status.split_whitespace();
|
|
parts.next().is_some_and(|version| version.starts_with("HTTP/")) && parts.next() == Some("200")
|
|
})
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::net::TcpListener;
|
|
|
|
/// Starts a server that answers one request with the given response, returns its port
|
|
fn serve_once(response: &'static str) -> u16 {
|
|
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
|
let port = listener.local_addr().unwrap().port();
|
|
|
|
std::thread::spawn(move || {
|
|
if let Ok((mut stream, _)) = listener.accept() {
|
|
let mut request = [0u8; 512];
|
|
let _ = stream.read(&mut request);
|
|
let _ = stream.write_all(response.as_bytes());
|
|
}
|
|
});
|
|
|
|
port
|
|
}
|
|
|
|
#[test]
|
|
fn healthy_when_server_answers_200() {
|
|
assert!(is_healthy(serve_once("HTTP/1.1 200 OK\r\ncontent-length: 2\r\n\r\nok")));
|
|
}
|
|
|
|
#[test]
|
|
fn unhealthy_when_database_is_down() {
|
|
assert!(!is_healthy(serve_once("HTTP/1.1 503 Service Unavailable\r\ncontent-length: 0\r\n\r\n")));
|
|
}
|
|
|
|
#[test]
|
|
fn unhealthy_on_other_answers() {
|
|
assert!(!is_healthy(serve_once("HTTP/1.1 302 Found\r\nlocation: /login\r\n\r\n")));
|
|
assert!(!is_healthy(serve_once("")));
|
|
assert!(!is_healthy(serve_once("garbage 200")));
|
|
}
|
|
|
|
#[test]
|
|
fn unhealthy_when_nothing_listens() {
|
|
// Take a free port and release it again
|
|
let port = TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port();
|
|
|
|
assert!(!is_healthy(port));
|
|
}
|
|
}
|