Coverage Report

Created: 2026-10-12 19:21

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-service/src/health_server.rs
Line
Count
Source
1
// Copyright 2024 The NativeLink Authors. All rights reserved.
2
//
3
// Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//    See LICENSE file for details
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
use core::convert::Infallible;
16
use core::future::Future;
17
use core::pin::Pin;
18
use core::task::{Context, Poll};
19
use core::time::Duration;
20
21
use axum::body::Body;
22
use bytes::Bytes;
23
use futures::StreamExt;
24
use http_body_util::Full;
25
use hyper::header::{CONTENT_TYPE, HeaderValue};
26
use hyper::{Request, Response, StatusCode};
27
use nativelink_config::cas_server::HealthConfig;
28
use nativelink_error::{Error, make_input_err};
29
use nativelink_util::health_utils::{
30
    HealthRegistry, HealthStatus, HealthStatusDescription, HealthStatusReporter,
31
};
32
use tower::Service;
33
use tracing::error_span;
34
35
/// Content type header value for JSON.
36
const JSON_CONTENT_TYPE: &str = "application/json; charset=utf-8";
37
38
// Note: This must be kept in sync with the documentation in
39
// `HealthConfig::timeout_seconds`.
40
const DEFAULT_HEALTH_CHECK_TIMEOUT_SECONDS: u64 = 5;
41
42
#[derive(Debug, Clone)]
43
pub struct HealthServer {
44
    health_registry: HealthRegistry,
45
    timeout: Duration,
46
    /// Readiness: a component still initializing makes the answer 503.
47
    strict: bool,
48
}
49
50
/// Where the plain status check answers when `path` is unset.
51
pub const DEFAULT_STATUS_PATH: &str = "/status";
52
/// Where the readiness check answers when `readiness_path` is unset.
53
pub const DEFAULT_READINESS_PATH: &str = "/ready";
54
55
/// The two paths the health service serves, defaults filled in, as
56
/// `(status, readiness)`. They must differ: the router panics on a
57
/// duplicate route, so a configuration that names the same path for both
58
/// is refused here with a message instead.
59
3
pub fn health_paths(health_cfg: &HealthConfig) -> Result<(String, String), Error> {
60
3
    let status = if health_cfg.path.is_empty() {
61
1
        DEFAULT_STATUS_PATH
62
    } else {
63
2
        &health_cfg.path
64
    };
65
3
    let readiness = if health_cfg.readiness_path.is_empty() {
66
2
        DEFAULT_READINESS_PATH
67
    } else {
68
1
        &health_cfg.readiness_path
69
    };
70
3
    if status == readiness {
71
1
        return Err(make_input_err!(
72
1
            "services.health.path and readiness_path are both {status}; the readiness check needs a path of its own"
73
1
        ));
74
2
    }
75
2
    Ok((status.to_string(), readiness.to_string()))
76
3
}
77
78
impl HealthServer {
79
5
    pub const fn new(health_registry: HealthRegistry, health_cfg: &HealthConfig) -> Self {
80
5
        let timeout = Duration::from_secs(if health_cfg.timeout_seconds == 0 {
81
4
            DEFAULT_HEALTH_CHECK_TIMEOUT_SECONDS
82
        } else {
83
1
            health_cfg.timeout_seconds
84
        });
85
5
        Self {
86
5
            health_registry,
87
5
            timeout,
88
5
            strict: false,
89
5
        }
90
5
    }
91
92
    /// The readiness form: unavailable while anything is initializing, not
93
    /// only when something failed.
94
0
    pub const fn readiness(health_registry: HealthRegistry, health_cfg: &HealthConfig) -> Self {
95
0
        let mut server = Self::new(health_registry, health_cfg);
96
0
        server.strict = true;
97
0
        server
98
0
    }
99
}
100
101
impl Service<Request<Body>> for HealthServer {
102
    type Response = Response<Full<Bytes>>;
103
    type Error = Infallible;
104
    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
105
106
5
    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
107
5
        Poll::Ready(Ok(()))
108
5
    }
109
110
5
    fn call(&mut self, _req: Request<Body>) -> Self::Future {
111
5
        let health_registry = self.health_registry.clone();
112
5
        let local_timeout = self.timeout;
113
5
        let strict = self.strict;
114
5
        Box::pin(error_span!("health_server_call").in_scope(|| async move {
115
5
            let health_status_descriptions: Vec<HealthStatusDescription> = health_registry
116
5
                .health_status_report(&local_timeout)
117
5
                .collect()
118
5
                .await;
119
120
5
            match serde_json5::to_string(&health_status_descriptions) {
121
5
                Ok(body) => {
122
5
                    let contains_failed_report =
123
5
                        health_status_descriptions.iter().any(|description| 
{4
124
4
                            matches!(description.status, HealthStatus::Failed { .. })
125
4
                                | 
matches!3
(description.status, HealthStatus::Timeout { .. })
126
4
                                | (strict
127
0
                                    && matches!(
128
1
                                        description.status,
129
                                        HealthStatus::Initializing { .. }
130
                                    ))
131
4
                        });
132
5
                    let status_code = if contains_failed_report {
133
2
                        StatusCode::SERVICE_UNAVAILABLE
134
                    } else {
135
3
                        StatusCode::OK
136
                    };
137
138
5
                    Ok(Response::builder()
139
5
                        .status(status_code)
140
5
                        .header(CONTENT_TYPE, HeaderValue::from_static(JSON_CONTENT_TYPE))
141
5
                        .body(Full::new(Bytes::from(body)))
142
5
                        .unwrap())
143
                }
144
145
0
                Err(e) => Ok(Response::builder()
146
0
                    .status(StatusCode::INTERNAL_SERVER_ERROR)
147
0
                    .header(CONTENT_TYPE, HeaderValue::from_static(JSON_CONTENT_TYPE))
148
0
                    .body(Full::new(Bytes::from(format!("Internal Failure: {e:?}"))))
149
0
                    .unwrap()),
150
            }
151
10
        }))
152
5
    }
153
}