1+ use std:: { collections:: HashMap , time:: Duration } ;
2+
13use axum:: { Json , extract:: State , http:: StatusCode } ;
4+ use broker:: RedisOperations ;
25use serde:: Serialize ;
6+ use tokio:: time:: timeout;
7+
8+ use crate :: state:: { AppState , HealthState } ;
39
4- use crate :: state:: AppState ;
10+ const REDIS_PING_TIMEOUT : u64 = 1 ;
11+
12+ #[ derive( Clone , Copy , Debug , PartialEq , Serialize ) ]
13+ #[ serde( rename_all = "lowercase" ) ]
14+ enum Status {
15+ Ok ,
16+ Fail ,
17+ }
518
619#[ derive( Serialize ) ]
720pub struct HealthResponse {
821 status : String ,
922 uptime : u64 ,
1023}
1124
12- pub async fn healthz_handler ( State ( state) : State < AppState > ) -> ( StatusCode , Json < HealthResponse > ) {
25+ pub async fn healthz_handler (
26+ State ( state) : State < HealthState > ,
27+ ) -> ( StatusCode , Json < HealthResponse > ) {
1328 let uptime = state. start_time . elapsed ( ) . as_secs ( ) ;
1429 (
1530 StatusCode :: OK ,
@@ -20,24 +35,91 @@ pub async fn healthz_handler(State(state): State<AppState>) -> (StatusCode, Json
2035 )
2136}
2237
38+ #[ derive( Serialize ) ]
39+ pub struct ReadyResponse {
40+ status : Status ,
41+ dependencies : HashMap < & ' static str , Status > ,
42+ }
43+
44+ async fn check_readiness < R : RedisOperations > ( redis : & R ) -> ReadyResponse {
45+ let mut deps = HashMap :: new ( ) ;
46+ let mut status = Status :: Ok ;
47+
48+ match timeout ( Duration :: from_secs ( REDIS_PING_TIMEOUT ) , redis. ping ( ) ) . await {
49+ Ok ( Ok ( _) ) => {
50+ deps. insert ( "redis" , Status :: Ok ) ;
51+ }
52+ _ => {
53+ deps. insert ( "redis" , Status :: Fail ) ;
54+ status = Status :: Fail ;
55+ }
56+ }
57+
58+ ReadyResponse {
59+ status,
60+ dependencies : deps,
61+ }
62+ }
63+
64+ pub async fn readyz_handler ( State ( state) : State < AppState > ) -> ( StatusCode , Json < ReadyResponse > ) {
65+ let response = check_readiness ( & state. redis ) . await ;
66+ let status_code = match response. status {
67+ Status :: Ok => StatusCode :: OK ,
68+ Status :: Fail => StatusCode :: SERVICE_UNAVAILABLE ,
69+ } ;
70+
71+ ( status_code, Json ( response) )
72+ }
73+
2374#[ cfg( test) ]
2475mod tests {
76+ use crate :: state:: HealthState ;
77+
2578 use super :: * ;
2679 use axum:: http:: StatusCode ;
80+ use broker:: { Error as BrokerError , MockRedisOperations } ;
2781 use tokio:: time:: Instant ;
2882
29- fn mock_app_state ( ) -> AppState {
30- AppState {
31- start_time : Instant :: now ( ) ,
32- }
33- }
34-
3583 #[ tokio:: test]
3684 async fn test_healthz_returns_ok ( ) {
37- let state = mock_app_state ( ) ;
85+ let state = HealthState {
86+ start_time : Instant :: now ( ) ,
87+ } ;
3888 let ( status, response) = healthz_handler ( State ( state) ) . await ;
3989
4090 assert_eq ! ( status, StatusCode :: OK ) ;
4191 assert_eq ! ( response. status, "ok" ) ;
4292 }
93+
94+ #[ tokio:: test]
95+ async fn test_readyz_redis_healthy ( ) {
96+ let mut mock_redis = MockRedisOperations :: new ( ) ;
97+
98+ mock_redis
99+ . expect_ping ( )
100+ . times ( 1 )
101+ . returning ( || Ok ( "PONG" . to_string ( ) ) ) ;
102+
103+ let response = check_readiness ( & mock_redis) . await ;
104+
105+ assert_eq ! ( response. status, Status :: Ok ) ;
106+ assert_eq ! ( response. dependencies. get( "redis" ) , Some ( & Status :: Ok ) ) ;
107+ }
108+
109+ #[ tokio:: test]
110+ async fn test_readyz_redis_error ( ) {
111+ let mut mock_redis = MockRedisOperations :: new ( ) ;
112+
113+ mock_redis. expect_ping ( ) . times ( 1 ) . returning ( || {
114+ Err ( BrokerError :: Redis ( redis:: RedisError :: from ( (
115+ redis:: ErrorKind :: IoError ,
116+ "Connection refused" ,
117+ ) ) ) )
118+ } ) ;
119+
120+ let response = check_readiness ( & mock_redis) . await ;
121+
122+ assert_eq ! ( response. status, Status :: Fail ) ;
123+ assert_eq ! ( response. dependencies. get( "redis" ) , Some ( & Status :: Fail ) ) ;
124+ }
43125}
0 commit comments