// Copyright (C) 2024-2026 Whiterun LLC // // This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later. // A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html use std::sync::atomic::Ordering; use std::sync::Arc; use rocket::http::{ContentType, Status}; use rocket::response::{self, Responder}; use rocket::{get, Request, State}; use serde_json::json; use crate::IbdState; /// Custom responder that returns the appropriate status code based on health pub struct HealthResult { healthy: bool, body: String, } impl<'r> Responder<'r, 'static> for HealthResult { fn respond_to(self, _request: &'r Request<'_>) -> response::Result<'static> { let status = if self.healthy { Status::Ok } else { Status::ServiceUnavailable }; response::Response::build() .status(status) .header(ContentType::JSON) .sized_body(self.body.len(), std::io::Cursor::new(self.body)) .ok() } } /// Health check endpoint for load balancer monitoring. /// /// Returns HTTP 200 when the indexer is ready to serve requests, /// or HTTP 503 when initial block download is in progress. /// /// **Response when healthy (HTTP 200):** /// ```json /// { /// "status": "healthy", /// "indexed_height": 880000, /// "chain_tip": 880000, /// "version": "0.2.0" /// } /// ``` /// /// **Response when syncing (HTTP 503):** /// ```json /// { /// "status": "syncing", /// "indexed_height": 850000, /// "chain_tip": 880000, /// "version": "0.2.0" /// } /// ``` #[get("/health")] pub fn health(ibd_state: &State>) -> HealthResult { let is_ready = ibd_state.initial_sync_complete.load(Ordering::Relaxed); let indexed_height = ibd_state.current_height.load(Ordering::Relaxed); let chain_tip = ibd_state.target_height.load(Ordering::Relaxed); let status = if is_ready { "healthy" } else { "syncing" }; let body = json!({ "status": status, "indexed_height": indexed_height, "chain_tip": chain_tip, "version": env!("CARGO_PKG_VERSION") }) .to_string(); HealthResult { healthy: is_ready, body, } } #[cfg(test)] mod tests { use super::*; use rocket::http::Status; use rocket::local::blocking::Client; use rocket::routes; use serde_json::Value; use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicU64; #[test] fn test_health_returns_503_during_ibd() { let ibd_state = Arc::new(IbdState { initial_sync_complete: AtomicBool::new(false), current_height: AtomicU64::new(850000), target_height: AtomicU64::new(880000), serve_during_ibd: false, }); let rocket = rocket::build() .manage(ibd_state) .mount("/", routes![health]); let client = Client::tracked(rocket).expect("valid rocket instance"); let response = client.get("/health").dispatch(); assert_eq!(response.status(), Status::ServiceUnavailable); let body: Value = serde_json::from_str(&response.into_string().unwrap()).unwrap(); assert_eq!(body["status"], "syncing"); assert_eq!(body["indexed_height"], 850000); assert_eq!(body["chain_tip"], 880000); assert_eq!(body["version"], env!("CARGO_PKG_VERSION")); } #[test] fn test_health_returns_200_when_ready() { let ibd_state = Arc::new(IbdState { initial_sync_complete: AtomicBool::new(true), current_height: AtomicU64::new(880000), target_height: AtomicU64::new(880000), serve_during_ibd: false, }); let rocket = rocket::build() .manage(ibd_state) .mount("/", routes![health]); let client = Client::tracked(rocket).expect("valid rocket instance"); let response = client.get("/health").dispatch(); assert_eq!(response.status(), Status::Ok); let body: Value = serde_json::from_str(&response.into_string().unwrap()).unwrap(); assert_eq!(body["status"], "healthy"); assert_eq!(body["indexed_height"], 880000); assert_eq!(body["chain_tip"], 880000); assert_eq!(body["version"], env!("CARGO_PKG_VERSION")); } }