1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::{Arc, OnceLock};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
6#[serde(rename_all = "snake_case")]
7pub enum HealthState {
8 Healthy,
10 Unhealthy,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
16pub struct HealthStatus {
17 pub status: HealthState,
19 pub message: Option<String>,
21}
22
23impl HealthStatus {
24 #[must_use]
26 pub const fn healthy() -> Self {
27 Self {
28 status: HealthState::Healthy,
29 message: None,
30 }
31 }
32
33 #[must_use]
35 pub fn unhealthy(message: impl Into<String>) -> Self {
36 Self {
37 status: HealthState::Unhealthy,
38 message: Some(message.into()),
39 }
40 }
41}
42
43#[must_use]
58pub const fn health_check() -> HealthStatus {
59 HealthStatus::healthy()
60}
61
62#[derive(Clone, Debug)]
70pub struct AdmissionReadiness {
71 available: Arc<AtomicBool>,
72}
73
74impl AdmissionReadiness {
75 #[must_use]
77 pub fn available() -> Self {
78 Self {
79 available: Arc::new(AtomicBool::new(true)),
80 }
81 }
82
83 #[must_use]
85 pub fn is_available(&self) -> bool {
86 self.available.load(Ordering::SeqCst)
87 }
88
89 pub fn set_available(&self, available: bool) {
91 self.available.store(available, Ordering::SeqCst);
92 }
93}
94
95impl Default for AdmissionReadiness {
96 fn default() -> Self {
97 Self::available()
98 }
99}
100
101#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
103pub enum ClusterReadiness {
104 #[default]
106 NotConfigured,
107 Configured {
109 membership_established: bool,
111 },
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub struct ReadinessState {
117 pub config_loaded: bool,
120 pub listener_bound: bool,
126 pub cluster: ClusterReadiness,
128 pub admission_available: bool,
134}
135
136impl ReadinessState {
137 #[must_use]
139 pub const fn new(config_loaded: bool, listener_bound: bool, cluster: ClusterReadiness) -> Self {
140 Self::with_admission(config_loaded, listener_bound, cluster, true)
141 }
142
143 #[must_use]
145 pub const fn with_admission(
146 config_loaded: bool,
147 listener_bound: bool,
148 cluster: ClusterReadiness,
149 admission_available: bool,
150 ) -> Self {
151 Self {
152 config_loaded,
153 listener_bound,
154 cluster,
155 admission_available,
156 }
157 }
158
159 #[must_use]
161 pub const fn ready_without_cluster() -> Self {
162 Self::new(true, true, ClusterReadiness::NotConfigured)
163 }
164
165 #[must_use]
167 pub const fn ready_with_cluster() -> Self {
168 Self::new(
169 true,
170 true,
171 ClusterReadiness::Configured {
172 membership_established: true,
173 },
174 )
175 }
176}
177
178impl Default for ReadinessState {
179 fn default() -> Self {
190 Self::with_admission(false, false, ClusterReadiness::NotConfigured, true)
191 }
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
196#[serde(rename_all = "snake_case")]
197pub enum ReadinessCondition {
198 ConfigLoaded,
200 ListenerBound,
202 ClusterMembershipEstablished,
204 AdmissionAvailable,
207}
208
209#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
211pub struct ReadinessStatus {
212 pub ready: bool,
214 pub unmet_conditions: Vec<ReadinessCondition>,
216}
217
218impl ReadinessStatus {
219 #[must_use]
221 pub fn from_unmet_conditions(unmet_conditions: Vec<ReadinessCondition>) -> Self {
222 Self {
223 ready: unmet_conditions.is_empty(),
224 unmet_conditions,
225 }
226 }
227}
228
229#[derive(Debug, Clone)]
231pub struct SharedReadinessState {
232 inner: Arc<ReadinessFlags>,
233}
234
235impl SharedReadinessState {
236 #[must_use]
238 pub fn new(initial: ReadinessState) -> Self {
239 Self {
240 inner: Arc::new(ReadinessFlags::from_state(initial)),
241 }
242 }
243
244 #[must_use]
246 pub fn snapshot(&self) -> ReadinessState {
247 let cluster = if self.inner.cluster_configured.load(Ordering::SeqCst) {
248 ClusterReadiness::Configured {
249 membership_established: self
250 .inner
251 .cluster_membership_established
252 .load(Ordering::SeqCst),
253 }
254 } else {
255 ClusterReadiness::NotConfigured
256 };
257
258 ReadinessState::with_admission(
259 self.inner.config_loaded.load(Ordering::SeqCst),
260 self.inner.listener_bound.load(Ordering::SeqCst),
261 cluster,
262 self.inner
263 .admission
264 .get()
265 .is_none_or(AdmissionReadiness::is_available),
266 )
267 }
268
269 pub fn track_admission(&self, admission: AdmissionReadiness) {
275 let _ = self.inner.admission.set(admission);
276 }
277
278 pub fn set_config_loaded(&self, loaded: bool) {
280 self.inner.config_loaded.store(loaded, Ordering::SeqCst);
281 }
282
283 pub fn set_listener_bound(&self, bound: bool) {
285 self.inner.listener_bound.store(bound, Ordering::SeqCst);
286 }
287
288 pub fn set_cluster_configured(&self, configured: bool) {
290 self.inner
291 .cluster_configured
292 .store(configured, Ordering::SeqCst);
293 if !configured {
294 self.set_cluster_membership_established(false);
295 }
296 }
297
298 pub fn set_cluster_membership_established(&self, established: bool) {
300 self.inner
301 .cluster_membership_established
302 .store(established, Ordering::SeqCst);
303 }
304}
305
306impl Default for SharedReadinessState {
307 fn default() -> Self {
308 Self::new(ReadinessState::default())
309 }
310}
311
312#[derive(Debug)]
313struct ReadinessFlags {
314 config_loaded: AtomicBool,
315 listener_bound: AtomicBool,
316 cluster_configured: AtomicBool,
317 cluster_membership_established: AtomicBool,
318 admission: OnceLock<AdmissionReadiness>,
327}
328
329impl ReadinessFlags {
330 const fn from_state(state: ReadinessState) -> Self {
331 let (cluster_configured, cluster_membership_established) = match state.cluster {
332 ClusterReadiness::NotConfigured => (false, false),
333 ClusterReadiness::Configured {
334 membership_established,
335 } => (true, membership_established),
336 };
337
338 Self {
339 config_loaded: AtomicBool::new(state.config_loaded),
340 listener_bound: AtomicBool::new(state.listener_bound),
341 cluster_configured: AtomicBool::new(cluster_configured),
342 cluster_membership_established: AtomicBool::new(cluster_membership_established),
343 admission: OnceLock::new(),
344 }
345 }
346}
347
348#[must_use]
350pub fn readiness_check(state: &ReadinessState) -> ReadinessStatus {
351 let mut unmet_conditions = Vec::new();
352
353 if !state.config_loaded {
354 unmet_conditions.push(ReadinessCondition::ConfigLoaded);
355 }
356
357 if !state.listener_bound {
358 unmet_conditions.push(ReadinessCondition::ListenerBound);
359 }
360
361 if state.cluster
362 == (ClusterReadiness::Configured {
363 membership_established: false,
364 })
365 {
366 unmet_conditions.push(ReadinessCondition::ClusterMembershipEstablished);
367 }
368
369 if !state.admission_available {
373 unmet_conditions.push(ReadinessCondition::AdmissionAvailable);
374 }
375
376 ReadinessStatus::from_unmet_conditions(unmet_conditions)
377}
378
379#[cfg(test)]
380mod tests {
381 use super::{
382 ClusterReadiness, HealthState, ReadinessCondition, ReadinessState, SharedReadinessState,
383 health_check, readiness_check,
384 };
385
386 #[test]
387 fn health_check_is_always_healthy_liveness() {
388 let status = health_check();
389
390 assert_eq!(status.status, HealthState::Healthy);
391 assert!(status.message.is_none());
392 }
393
394 #[test]
395 fn readiness_reports_missing_config() {
396 let state = ReadinessState::new(false, true, ClusterReadiness::NotConfigured);
397
398 let status = readiness_check(&state);
399
400 assert!(!status.ready);
401 assert_eq!(
402 status.unmet_conditions,
403 vec![ReadinessCondition::ConfigLoaded]
404 );
405 }
406
407 #[test]
408 fn readiness_reports_missing_listener() {
409 let state = ReadinessState::new(true, false, ClusterReadiness::NotConfigured);
410
411 let status = readiness_check(&state);
412
413 assert!(!status.ready);
414 assert_eq!(
415 status.unmet_conditions,
416 vec![ReadinessCondition::ListenerBound]
417 );
418 }
419
420 #[test]
421 fn readiness_requires_cluster_membership_when_configured() {
422 let state = ReadinessState::new(
423 true,
424 true,
425 ClusterReadiness::Configured {
426 membership_established: false,
427 },
428 );
429
430 let status = readiness_check(&state);
431
432 assert!(!status.ready);
433 assert_eq!(
434 status.unmet_conditions,
435 vec![ReadinessCondition::ClusterMembershipEstablished]
436 );
437 }
438
439 #[test]
440 fn readiness_ignores_cluster_membership_when_not_configured() {
441 let state = ReadinessState::ready_without_cluster();
442
443 let status = readiness_check(&state);
444
445 assert!(status.ready);
446 assert!(status.unmet_conditions.is_empty());
447 }
448
449 #[test]
450 fn readiness_is_ready_only_when_all_applicable_conditions_are_met() {
451 let state = ReadinessState::ready_with_cluster();
452
453 let status = readiness_check(&state);
454
455 assert!(status.ready);
456 assert!(status.unmet_conditions.is_empty());
457 }
458
459 #[test]
460 fn shared_readiness_state_snapshots_updates() {
461 let shared = SharedReadinessState::default();
462 shared.set_config_loaded(true);
463 shared.set_listener_bound(true);
464 shared.set_cluster_configured(true);
465 shared.set_cluster_membership_established(true);
466
467 assert_eq!(shared.snapshot(), ReadinessState::ready_with_cluster());
468 }
469}