riftenlabs-indexer/src/rpc/health.rs

144 lines
4.3 KiB
Rust
Raw Normal View History

2026-02-10 15:36:04 +01:00
// 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<Arc<IbdState>>) -> 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"));
}
}