11//! HTTP `/health` endpoint as a tower middleware layer.
22//!
33//! Short-circuits `GET /health` before the jsonrpsee service sees the request
4- //! (which would 405 a GET). Any other request passes through unchanged.
4+ //! (which would 405 a GET). With a `SaturationMonitor` attached, returns 503
5+ //! once the service has been continuously rejecting requests for the
6+ //! configured threshold so load balancers can drain the pod. Body is opaque
7+ //! (no timestamps, counters, or upstream URLs).
58
69use std:: task:: { Context , Poll } ;
10+ use std:: time:: Duration ;
711
812use bytes:: Bytes ;
913use futures:: future:: { ready, Either , Ready } ;
@@ -12,28 +16,73 @@ use http_body_util::Full;
1216use jsonrpsee:: server:: HttpBody ;
1317use tower:: { Layer , Service } ;
1418
19+ use crate :: server:: saturation:: SaturationMonitor ;
20+
1521#[ cfg( test) ]
1622#[ path = "health_test.rs" ]
1723mod health_test;
1824
1925pub const HEALTH_PATH : & str = "/health" ;
2026
2127const HEALTHY_BODY : & [ u8 ] = br#"{"status":"ok"}"# ;
28+ /// Body returned by `GET /health` when saturated. Reason is an opaque code,
29+ /// no internal state included.
30+ const SATURATED_BODY : & [ u8 ] = br#"{"status":"unhealthy","reason":"saturated"}"# ;
31+
32+ /// `saturation: None` keeps the original always-200 behaviour;
33+ /// `Some(monitor)` flips to `503` once `monitor.saturated_for_at_least`
34+ /// crosses `saturation_threshold`.
35+ #[ derive( Clone , Default ) ]
36+ pub struct HealthLayer {
37+ saturation : Option < SaturationMonitor > ,
38+ saturation_threshold : Duration ,
39+ }
2240
23- #[ derive( Clone , Copy , Default ) ]
24- pub struct HealthLayer ;
41+ impl HealthLayer {
42+ pub fn with_saturation ( monitor : SaturationMonitor , threshold : Duration ) -> Self {
43+ Self { saturation : Some ( monitor) , saturation_threshold : threshold }
44+ }
45+ }
2546
2647impl < S > Layer < S > for HealthLayer {
2748 type Service = HealthService < S > ;
2849
2950 fn layer ( & self , inner : S ) -> Self :: Service {
30- HealthService { inner }
51+ HealthService {
52+ inner,
53+ saturation : self . saturation . clone ( ) ,
54+ saturation_threshold : self . saturation_threshold ,
55+ }
3156 }
3257}
3358
3459#[ derive( Clone ) ]
3560pub struct HealthService < S > {
3661 inner : S ,
62+ saturation : Option < SaturationMonitor > ,
63+ saturation_threshold : Duration ,
64+ }
65+
66+ impl < S > HealthService < S > {
67+ fn health_response ( & self ) -> Response < HttpBody > {
68+ let saturated = self
69+ . saturation
70+ . as_ref ( )
71+ . is_some_and ( |monitor| monitor. saturated_for_at_least ( self . saturation_threshold ) ) ;
72+ if saturated {
73+ Response :: builder ( )
74+ . status ( StatusCode :: SERVICE_UNAVAILABLE )
75+ . header ( header:: CONTENT_TYPE , "application/json" )
76+ . body ( HttpBody :: new ( Full :: new ( Bytes :: from_static ( SATURATED_BODY ) ) ) )
77+ . expect ( "response build with a static body is infallible" )
78+ } else {
79+ Response :: builder ( )
80+ . status ( StatusCode :: OK )
81+ . header ( header:: CONTENT_TYPE , "application/json" )
82+ . body ( HttpBody :: new ( Full :: new ( Bytes :: from_static ( HEALTHY_BODY ) ) ) )
83+ . expect ( "response build with a static body is infallible" )
84+ }
85+ }
3786}
3887
3988impl < S , ReqB > Service < Request < ReqB > > for HealthService < S >
@@ -52,12 +101,7 @@ where
52101
53102 fn call ( & mut self , request : Request < ReqB > ) -> Self :: Future {
54103 if request. method ( ) == Method :: GET && request. uri ( ) . path ( ) == HEALTH_PATH {
55- let response = Response :: builder ( )
56- . status ( StatusCode :: OK )
57- . header ( header:: CONTENT_TYPE , "application/json" )
58- . body ( HttpBody :: new ( Full :: new ( Bytes :: from_static ( HEALTHY_BODY ) ) ) )
59- . expect ( "response build with a static body is infallible" ) ;
60- return Either :: Left ( ready ( Ok ( response) ) ) ;
104+ return Either :: Left ( ready ( Ok ( self . health_response ( ) ) ) ) ;
61105 }
62106 Either :: Right ( self . inner . call ( request) )
63107 }
0 commit comments