Added health check and deployment setup for one container per customer. Fixes #12
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user