11use std:: sync:: Arc ;
2- use std:: sync:: atomic:: Ordering ;
2+ use std:: sync:: atomic:: { AtomicI64 , Ordering } ;
3+ use std:: time:: Duration ;
34
5+ use axum:: extract:: State ;
6+ use axum:: http:: StatusCode ;
47use axum:: { Router , routing:: get} ;
58use futures:: StreamExt ;
69use k8s_openapi:: api:: core:: v1:: ConfigMap ;
@@ -84,6 +87,18 @@ async fn main() -> anyhow::Result<()> {
8487
8588 let ctx = Arc :: new ( Context :: new ( client. clone ( ) , max_concurrent_restores) ) ;
8689
90+ // Heartbeat: a background task updates this timestamp every 5s.
91+ // If the runtime is deadlocked, the timestamp goes stale and /livez fails.
92+ let heartbeat = Arc :: new ( AtomicI64 :: new ( chrono:: Utc :: now ( ) . timestamp ( ) ) ) ;
93+ let heartbeat_writer = heartbeat. clone ( ) ;
94+ tokio:: spawn ( async move {
95+ let mut interval = tokio:: time:: interval ( Duration :: from_secs ( 5 ) ) ;
96+ loop {
97+ interval. tick ( ) . await ;
98+ heartbeat_writer. store ( chrono:: Utc :: now ( ) . timestamp ( ) , Ordering :: Relaxed ) ;
99+ }
100+ } ) ;
101+
87102 // Watch the operator ConfigMap for dynamic config updates
88103 let config_api: Api < ConfigMap > = Api :: namespaced ( client. clone ( ) , & namespace) ;
89104 let config_watcher_config =
@@ -128,6 +143,10 @@ async fn main() -> anyhow::Result<()> {
128143 // Start metrics server
129144 let metrics_registry = ctx. metrics . registry . clone ( ) ;
130145 let metrics_addr_clone = metrics_addr. clone ( ) ;
146+ let probe_state = ProbeState {
147+ client : client. clone ( ) ,
148+ heartbeat,
149+ } ;
131150 tokio:: spawn ( async move {
132151 let app = Router :: new ( )
133152 . route (
@@ -143,20 +162,9 @@ async fn main() -> anyhow::Result<()> {
143162 }
144163 } ) ,
145164 )
146- . route (
147- "/livez" ,
148- get ( || async {
149- tracing:: trace!( "GET /livez" ) ;
150- "ok"
151- } ) ,
152- )
153- . route (
154- "/readyz" ,
155- get ( || async {
156- tracing:: trace!( "GET /readyz" ) ;
157- "ok"
158- } ) ,
159- ) ;
165+ . route ( "/livez" , get ( livez) )
166+ . route ( "/readyz" , get ( readyz) )
167+ . with_state ( probe_state) ;
160168
161169 let listener = tokio:: net:: TcpListener :: bind ( & metrics_addr_clone)
162170 . await
@@ -221,3 +229,40 @@ async fn main() -> anyhow::Result<()> {
221229
222230 Ok ( ( ) )
223231}
232+
233+ #[ derive( Clone ) ]
234+ struct ProbeState {
235+ client : Client ,
236+ heartbeat : Arc < AtomicI64 > ,
237+ }
238+
239+ /// Liveness: checks that the async runtime isn't deadlocked by verifying
240+ /// a background heartbeat was updated within the last 30 seconds.
241+ async fn livez ( State ( state) : State < ProbeState > ) -> ( StatusCode , & ' static str ) {
242+ let last = state. heartbeat . load ( Ordering :: Relaxed ) ;
243+ let age = chrono:: Utc :: now ( ) . timestamp ( ) - last;
244+ tracing:: trace!( heartbeat_age_secs = age, "GET /livez" ) ;
245+ if age <= 30 {
246+ ( StatusCode :: OK , "ok" )
247+ } else {
248+ tracing:: warn!(
249+ heartbeat_age_secs = age,
250+ "liveness check failed: heartbeat stale"
251+ ) ;
252+ ( StatusCode :: INTERNAL_SERVER_ERROR , "heartbeat stale" )
253+ }
254+ }
255+
256+ /// Readiness: checks that the Kubernetes API server is reachable.
257+ async fn readyz ( State ( state) : State < ProbeState > ) -> ( StatusCode , & ' static str ) {
258+ match state. client . apiserver_version ( ) . await {
259+ Ok ( _) => {
260+ tracing:: trace!( "GET /readyz" ) ;
261+ ( StatusCode :: OK , "ok" )
262+ }
263+ Err ( e) => {
264+ tracing:: warn!( error = %e, "readiness check failed: API server unreachable" ) ;
265+ ( StatusCode :: SERVICE_UNAVAILABLE , "API server unreachable" )
266+ }
267+ }
268+ }
0 commit comments