Skip to main content

aion_server/worker/supervisor/
status.rs

1//! What the server will say about a managed worker, and what it refuses to say.
2//!
3//! The status is a JOIN of two different kinds of truth: the durable record's
4//! desired state, and the live instance's actual state. Reporting one without
5//! the other is how "it says stopped" comes to mean "nobody looked". Every
6//! actual state below is written by the instance loop only after the fact it
7//! names has been established — `Running` after a spawn returned a pid,
8//! `Stopped` only after the process group was probed empty.
9
10use aion_store::{DeployedBinaryIdentity, DesiredState};
11use serde::{Deserialize, Serialize};
12
13/// What a managed worker instance is actually doing right now.
14#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
15#[serde(rename_all = "kebab-case")]
16pub enum ManagedWorkerState {
17    /// A spawn is in progress; no pid has been observed yet.
18    Starting,
19    /// A contained child is alive and leading its process group.
20    Running,
21    /// The child exited and a restart is waiting out the configured backoff.
22    Backoff,
23    /// No process is running and none is wanted: either the deployment desires
24    /// `Stopped`, or a stop confirmed the process group empty.
25    Stopped,
26    /// Supervision gave up: the restart budget for the window was exhausted, or
27    /// stopping could not be confirmed. Terminal until an operator acts.
28    Failed,
29    /// The deployment desires `Running` but this server has no supervision
30    /// policy, so nothing is supervising it. The remedy travels with the report.
31    Uncommissioned,
32}
33
34impl ManagedWorkerState {
35    /// Stable lowercase token used on the wire and in status history.
36    #[must_use]
37    pub const fn token(self) -> &'static str {
38        match self {
39            Self::Starting => "starting",
40            Self::Running => "running",
41            Self::Backoff => "backoff",
42            Self::Stopped => "stopped",
43            Self::Failed => "failed",
44            Self::Uncommissioned => "uncommissioned",
45        }
46    }
47
48    /// Whether the supervisor has stopped acting on this instance.
49    #[must_use]
50    pub const fn is_terminal(self) -> bool {
51        matches!(self, Self::Stopped | Self::Failed)
52    }
53}
54
55/// Identity of the executable one spawn actually ran.
56///
57/// The content hash is taken from the bytes on disk AT SPAWN, which is the
58/// whole point: a restart that picks up an upgraded binary changes this hash
59/// while the deployment record's deploy-time hash stays put, so the swap is a
60/// visible, reportable fact instead of an invisible one.
61#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
62pub struct SpawnedBinary {
63    /// Path of the executable that was launched.
64    pub path: String,
65    /// Lowercase hexadecimal SHA-256 digest of the executable bytes at spawn.
66    pub content_hash: String,
67    /// Package version stamped into the binary. Present only when the spawned
68    /// executable is this server's own binary, which is the only executable
69    /// whose build stamp this process can honestly claim to know.
70    pub version: Option<String>,
71    /// Source commit stamped into the binary, under the same condition.
72    pub commit: Option<String>,
73    /// Dirty-state token stamped into the binary, under the same condition.
74    pub dirty: Option<String>,
75}
76
77/// How a managed worker process last ended.
78#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
79pub struct ManagedWorkerExit {
80    /// When the exit was observed.
81    pub at: chrono::DateTime<chrono::Utc>,
82    /// Exit code, when the process exited normally.
83    pub code: Option<i32>,
84    /// Terminating signal number, when a signal ended the process.
85    pub signal: Option<i32>,
86    /// Whether the supervisor itself asked for this ending.
87    pub requested: bool,
88}
89
90/// One managed worker: what was asked for, and what is true.
91#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
92pub struct ManagedWorkerStatus {
93    /// Deployment primary key.
94    pub name: String,
95    /// Task queue the deployment binds.
96    pub task_queue: String,
97    /// Durable operator intent.
98    pub desired: DesiredState,
99    /// Live supervision state.
100    pub state: ManagedWorkerState,
101    /// Process id of the running child, while one is running.
102    pub pid: Option<u32>,
103    /// Process group id the child leads, while one is running.
104    pub process_group: Option<i32>,
105    /// Restarts performed since this instance was started.
106    pub restarts: u32,
107    /// How the process last ended, when it has ended at least once.
108    pub last_exit: Option<ManagedWorkerExit>,
109    /// The last failure worth an operator's attention, verbatim.
110    pub last_error: Option<String>,
111    /// Identity of the binary the deployment named AT DEPLOY TIME.
112    pub deployed_binary: DeployedBinaryIdentity,
113    /// Identity of the binary the CURRENT (or most recent) spawn actually ran.
114    ///
115    /// A restart across an upgrade is meant to be a visible fact rather than a
116    /// silent swap, so this is captured per spawn and reported beside the
117    /// deploy-time identity: the two disagreeing is the upgrade, stated.
118    pub spawn_binary: Option<SpawnedBinary>,
119}
120
121/// The whole managed-worker surface for this server.
122#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
123pub struct ManagedWorkerReport {
124    /// Whether a supervision policy is configured on this server.
125    pub commissioned: bool,
126    /// The remedy for an uncommissioned server, present exactly when
127    /// `commissioned` is false. Absent when there is nothing to remedy.
128    pub remedy: Option<String>,
129    /// One entry per durable deployment, ordered by name.
130    pub workers: Vec<ManagedWorkerStatus>,
131    /// Names of durable rows that are PRESENT but could not be decoded, and are
132    /// therefore supervised by nothing. Reported rather than dropped: a row the
133    /// server cannot read is exactly the row an operator needs told about.
134    pub undecodable: Vec<String>,
135}
136
137#[cfg(test)]
138mod tests {
139    use super::ManagedWorkerState;
140
141    #[test]
142    fn tokens_are_distinct_and_terminality_matches_them() {
143        let states = [
144            ManagedWorkerState::Starting,
145            ManagedWorkerState::Running,
146            ManagedWorkerState::Backoff,
147            ManagedWorkerState::Stopped,
148            ManagedWorkerState::Failed,
149            ManagedWorkerState::Uncommissioned,
150        ];
151        let mut tokens = states.map(ManagedWorkerState::token).to_vec();
152        tokens.sort_unstable();
153        tokens.dedup();
154        assert_eq!(tokens.len(), states.len());
155
156        assert!(ManagedWorkerState::Stopped.is_terminal());
157        assert!(ManagedWorkerState::Failed.is_terminal());
158        assert!(!ManagedWorkerState::Running.is_terminal());
159        assert!(!ManagedWorkerState::Backoff.is_terminal());
160        assert!(!ManagedWorkerState::Starting.is_terminal());
161        // Uncommissioned is not terminal: commissioning the server and starting
162        // the deployment is exactly the state it is waiting for.
163        assert!(!ManagedWorkerState::Uncommissioned.is_terminal());
164    }
165}