Skip to content

Commit f11ba66

Browse files
committed
add some actual healthchecks
1 parent 313b42c commit f11ba66

1 file changed

Lines changed: 60 additions & 15 deletions

File tree

src/bin/operator.rs

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use 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;
47
use axum::{Router, routing::get};
58
use futures::StreamExt;
69
use 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

Comments
 (0)