iota_rest_api/
health.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::time::{Duration, SystemTime};
6
7use axum::{
8    extract::{Query, State},
9    http::StatusCode,
10    response::IntoResponse,
11};
12use iota_types::storage::ReadStore;
13use tap::Pipe;
14
15use crate::{
16    RestService, Result,
17    openapi::{ApiEndpoint, OperationBuilder, ResponseBuilder, RouteHandler},
18    reader::StateReader,
19};
20
21/// Perform a service health check
22///
23/// By default the health check only verifies that the latest checkpoint can be
24/// fetched from the node's store before returning a 200. Optionally the
25/// `threshold_seconds` parameter can be provided to test for how up to date the
26/// node needs to be to be considered healthy.
27pub struct HealthCheck;
28
29impl ApiEndpoint<RestService> for HealthCheck {
30    fn method(&self) -> axum::http::Method {
31        axum::http::Method::GET
32    }
33
34    fn path(&self) -> &'static str {
35        "/-/health"
36    }
37
38    fn stable(&self) -> bool {
39        true
40    }
41
42    fn operation(
43        &self,
44        generator: &mut schemars::gen::SchemaGenerator,
45    ) -> openapiv3::v3_1::Operation {
46        OperationBuilder::new()
47            .tag("General")
48            .operation_id("Health Check")
49            .query_parameters::<Threshold>(generator)
50            .response(200, ResponseBuilder::new().text_content().build())
51            .response(500, ResponseBuilder::new().build())
52            .build()
53    }
54
55    fn handler(&self) -> crate::openapi::RouteHandler<RestService> {
56        RouteHandler::new(self.method(), health)
57    }
58}
59
60#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
61pub struct Threshold {
62    /// The threshold, or delta, between the server's system time and the
63    /// timestamp in the most recently executed checkpoint for which the
64    /// server is considered to be healthy.
65    ///
66    /// If not provided, the server will be considered healthy if it can simply
67    /// fetch the latest checkpoint from its store.
68    pub threshold_seconds: Option<u32>,
69}
70
71async fn health(
72    Query(Threshold { threshold_seconds }): Query<Threshold>,
73    State(state): State<StateReader>,
74) -> Result<impl IntoResponse> {
75    let summary = state.inner().get_latest_checkpoint()?;
76
77    // If we have a provided threshold, check that it's close to the current time
78    if let Some(threshold_seconds) = threshold_seconds {
79        let latest_chain_time = summary.timestamp();
80
81        let threshold = SystemTime::now() - Duration::from_secs(threshold_seconds as u64);
82
83        if latest_chain_time < threshold {
84            return Err(anyhow::anyhow!(
85                "The latest checkpoint timestamp is less than the provided threshold"
86            )
87            .into());
88        }
89    }
90
91    StatusCode::OK.pipe(Ok)
92}