144 lines
4.3 KiB
Rust
144 lines
4.3 KiB
Rust
|
|
// 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"));
|
||
|
|
}
|
||
|
|
}
|