Skip to main content

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}