clawft_kernel/workload_kind/health.rs
1//! Kind-defined instance health (ADR-099 section 7: "instance health from a
2//! periodic status heartbeat whose meaning is kind-defined").
3//!
4//! A kind says how often its instances are polled, how many bad polls in a
5//! row make one unhealthy, and how a status sample is judged. The sample is
6//! deliberately small and runtime-agnostic (the adapter's `InstanceStatus`
7//! is mapped onto it by the host), so a kind defines health without
8//! depending on any adapter.
9
10use serde::{Deserialize, Serialize};
11
12/// How a kind's instances are polled.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14pub struct HealthSpec {
15 /// Poll period.
16 pub interval_ms: u64,
17 /// Consecutive bad polls that make an instance unhealthy.
18 pub miss_limit: u32,
19}
20
21impl Default for HealthSpec {
22 fn default() -> Self {
23 Self {
24 interval_ms: 10_000,
25 miss_limit: 3,
26 }
27 }
28}
29
30/// The adapter state a poll saw.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub enum SampleState {
34 /// Loaded, never started.
35 Loaded,
36 /// Running.
37 Running,
38 /// Exited (finished or stopped).
39 Exited,
40 /// The adapter reports it degraded.
41 Degraded,
42 /// The adapter does not know it.
43 Unknown,
44}
45
46/// One poll of an instance.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct HealthSample {
49 /// State the adapter reported.
50 pub state: SampleState,
51 /// Exit code when it exited.
52 pub exit_code: Option<i32>,
53 /// The instance is meant to run until stopped (listener or interval
54 /// mode); false for a one-shot run that ends by itself.
55 pub continuous: bool,
56}
57
58/// A kind's verdict on one poll.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum Health {
61 /// Fine: resets the miss count.
62 Healthy,
63 /// A one-shot run ended cleanly: nothing to restart.
64 Finished,
65 /// Not fine; counts as a miss. The text is chained.
66 Unhealthy(String),
67}
68
69/// The default judgment (a process-backed workload): running is healthy, a
70/// degraded or unexpectedly exited process is not, an unknown instance is a
71/// miss, and a clean one-shot exit is finished.
72pub fn judge_process(s: &HealthSample) -> Health {
73 match s.state {
74 SampleState::Running => Health::Healthy,
75 SampleState::Loaded => Health::Healthy,
76 SampleState::Degraded => Health::Unhealthy("adapter reports it degraded".into()),
77 SampleState::Unknown => Health::Unhealthy("adapter does not know the instance".into()),
78 SampleState::Exited if !s.continuous && s.exit_code == Some(0) => Health::Finished,
79 SampleState::Exited => Health::Unhealthy(match s.exit_code {
80 Some(c) => format!("exited with code {c}"),
81 None => "exited (killed by a signal)".into(),
82 }),
83 }
84}