gatehouse_runtime_protocol/lifecycle/
state.rs1use crate::{digest, Invalid};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "kebab-case")]
8pub enum Phase {
9 PendingCreate,
10 UnresolvedNoId,
11 KnownCreated,
12 Starting,
13 Running,
14 CheckingHealth,
15 Retained,
16 Stopping,
17 Stopped,
18 Removing,
19 AwaitingAbsence,
20 ReconcilingRemove,
21 VerifiedAbsent,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(deny_unknown_fields)]
26pub struct State {
27 phase: Phase,
28 object_id: Option<String>,
29}
30
31#[derive(Debug)]
34pub enum Event {
35 Created(String),
36 BeginStart,
37 RunningVerified,
38 BeginHealth,
39 HealthVerified,
40 Failed,
41 CleanupRunningVerified,
42 CleanupTerminalVerified,
43 StoppedVerified,
44 BeginRemove,
45 RemoveClaimed,
46 AbsenceVerified,
47 OperatorCleanupRunningVerified,
48 OperatorCleanupTerminalVerified,
49 OperatorAbsenceVerified,
50}
51
52impl State {
53 pub fn pending_create() -> Self {
54 Self {
55 phase: Phase::PendingCreate,
56 object_id: None,
57 }
58 }
59
60 pub fn phase(&self) -> Phase {
61 self.phase
62 }
63 pub fn object_id(&self) -> Option<&str> {
64 self.object_id.as_deref()
65 }
66
67 pub fn validate(&self) -> Result<(), Invalid> {
68 match (&self.phase, &self.object_id) {
69 (Phase::PendingCreate | Phase::UnresolvedNoId, None) => Ok(()),
70 (Phase::PendingCreate | Phase::UnresolvedNoId, Some(_)) | (_, None) => {
71 Err(Invalid("invalid lifecycle state identity"))
72 }
73 (_, Some(id)) => digest(id),
74 }
75 }
76
77 pub fn blocks_acquisition(&self) -> bool {
78 self.validate().is_err() || self.phase != Phase::VerifiedAbsent
79 }
80
81 pub fn recover(&self) -> Result<Self, Invalid> {
83 self.validate()?;
84 let mut next = self.clone();
85 next.phase = match self.phase {
86 Phase::PendingCreate | Phase::UnresolvedNoId => Phase::UnresolvedNoId,
87 Phase::Removing | Phase::AwaitingAbsence | Phase::ReconcilingRemove => {
88 Phase::ReconcilingRemove
89 }
90 Phase::VerifiedAbsent => Phase::VerifiedAbsent,
91 _ => Phase::Retained,
92 };
93 Ok(next)
94 }
95
96 pub fn transition(&self, event: Event) -> Result<Self, Invalid> {
97 self.validate()?;
98 let mut next = self.clone();
99 next.phase = match (self.phase, event) {
100 (Phase::PendingCreate, Event::Created(id)) => {
101 digest(&id)?;
102 next.object_id = Some(id);
103 Phase::KnownCreated
104 }
105 (Phase::PendingCreate, Event::Failed) => Phase::UnresolvedNoId,
106 (Phase::KnownCreated, Event::BeginStart) => Phase::Starting,
107 (Phase::Starting, Event::RunningVerified) => Phase::Running,
108 (Phase::Running, Event::BeginHealth) => Phase::CheckingHealth,
109 (Phase::CheckingHealth, Event::HealthVerified) => Phase::Running,
110 (
111 Phase::KnownCreated | Phase::Running | Phase::Retained,
112 Event::CleanupRunningVerified,
113 ) => Phase::Stopping,
114 (
115 Phase::KnownCreated | Phase::Running | Phase::Retained,
116 Event::CleanupTerminalVerified,
117 ) => Phase::Stopped,
118 (Phase::ReconcilingRemove, Event::OperatorCleanupRunningVerified) => Phase::Stopping,
119 (Phase::ReconcilingRemove, Event::OperatorCleanupTerminalVerified) => Phase::Stopped,
120 (Phase::Retained, Event::OperatorAbsenceVerified) => Phase::VerifiedAbsent,
121 (Phase::Stopping, Event::StoppedVerified) => Phase::Stopped,
122 (Phase::Stopped, Event::BeginRemove) => Phase::Removing,
123 (Phase::Removing, Event::RemoveClaimed) => Phase::AwaitingAbsence,
124 (Phase::AwaitingAbsence | Phase::ReconcilingRemove, Event::AbsenceVerified) => {
125 Phase::VerifiedAbsent
126 }
127 (
128 Phase::Removing | Phase::AwaitingAbsence | Phase::ReconcilingRemove,
129 Event::Failed,
130 ) => Phase::ReconcilingRemove,
131 (
132 Phase::KnownCreated
133 | Phase::Starting
134 | Phase::Running
135 | Phase::CheckingHealth
136 | Phase::Retained
137 | Phase::Stopping
138 | Phase::Stopped,
139 Event::Failed,
140 ) => Phase::Retained,
141 _ => return Err(Invalid("invalid lifecycle transition")),
142 };
143 next.validate()?;
144 Ok(next)
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 fn created() -> State {
152 State::pending_create()
153 .transition(Event::Created("a".repeat(64)))
154 .unwrap()
155 }
156
157 #[test]
158 fn lost_create_response_blocks_reacquisition_and_replay() {
159 let state = State::pending_create().recover().unwrap();
160 assert_eq!(state.phase(), Phase::UnresolvedNoId);
161 assert!(state.blocks_acquisition());
162 assert!(state.transition(Event::Created("b".repeat(64))).is_err());
163 assert!(state.transition(Event::BeginStart).is_err());
164 assert!(state.transition(Event::AbsenceVerified).is_err());
165 }
166
167 #[test]
168 fn cleanup_requires_stop_and_independent_absence() {
169 let running = created()
170 .transition(Event::BeginStart)
171 .unwrap()
172 .transition(Event::RunningVerified)
173 .unwrap();
174 assert!(running.transition(Event::BeginRemove).is_err());
175 let removed = running
176 .transition(Event::CleanupRunningVerified)
177 .unwrap()
178 .transition(Event::StoppedVerified)
179 .unwrap()
180 .transition(Event::BeginRemove)
181 .unwrap()
182 .transition(Event::RemoveClaimed)
183 .unwrap();
184 assert!(removed.blocks_acquisition());
185 let absent = removed.transition(Event::AbsenceVerified).unwrap();
186 assert!(!absent.blocks_acquisition());
187 assert!(absent.transition(Event::BeginStart).is_err());
188 }
189
190 #[test]
191 fn crash_with_known_id_retains_identity_and_cannot_restart() {
192 for phase in [
193 Phase::KnownCreated,
194 Phase::Starting,
195 Phase::Running,
196 Phase::CheckingHealth,
197 Phase::Stopping,
198 Phase::Stopped,
199 ] {
200 let state = State {
201 phase,
202 object_id: Some("c".repeat(64)),
203 };
204 let recovered = state.recover().unwrap();
205 assert_eq!(recovered.phase(), Phase::Retained);
206 assert_eq!(recovered.object_id(), state.object_id());
207 assert!(recovered.blocks_acquisition());
208 assert!(recovered.transition(Event::BeginStart).is_err());
209 assert!(recovered.transition(Event::AbsenceVerified).is_err());
210 }
211 }
212
213 #[test]
214 fn corrupt_state_and_partial_ids_never_unlock_or_transition() {
215 for state in [
216 State {
217 phase: Phase::VerifiedAbsent,
218 object_id: None,
219 },
220 State {
221 phase: Phase::PendingCreate,
222 object_id: Some("d".repeat(64)),
223 },
224 State {
225 phase: Phase::Running,
226 object_id: Some("short".into()),
227 },
228 ] {
229 assert!(state.blocks_acquisition());
230 assert!(state.recover().is_err());
231 assert!(state.transition(Event::BeginRemove).is_err());
232 }
233 assert!(State::pending_create()
234 .transition(Event::Created("abc".into()))
235 .is_err());
236 }
237
238 #[test]
239 fn failed_health_retains_id_without_cleanup_or_retry() {
240 let state = created()
241 .transition(Event::BeginStart)
242 .unwrap()
243 .transition(Event::RunningVerified)
244 .unwrap()
245 .transition(Event::BeginHealth)
246 .unwrap()
247 .transition(Event::Failed)
248 .unwrap();
249 assert_eq!(state.phase(), Phase::Retained);
250 assert!(state.object_id().is_some());
251 assert!(state.transition(Event::BeginHealth).is_err());
252 assert!(state.transition(Event::BeginRemove).is_err());
253 }
254 #[test]
255 fn every_post_remove_crash_window_reconciles_without_second_remove() {
256 for phase in [
260 Phase::Removing,
261 Phase::AwaitingAbsence,
262 Phase::ReconcilingRemove,
263 ] {
264 let state = State {
265 phase,
266 object_id: Some("d".repeat(64)),
267 };
268 let recovered = state.recover().unwrap();
269 assert_eq!(recovered.phase(), Phase::ReconcilingRemove);
270 assert_eq!(recovered.object_id(), state.object_id());
271 assert!(recovered.blocks_acquisition());
272 assert!(recovered.transition(Event::BeginRemove).is_err());
273 assert!(recovered.transition(Event::RemoveClaimed).is_err());
274 assert!(recovered.transition(Event::RunningVerified).is_err());
275 assert!(recovered
276 .transition(Event::CleanupTerminalVerified)
277 .is_err());
278 let failed = recovered.transition(Event::Failed).unwrap();
279 assert_eq!(failed.phase(), Phase::ReconcilingRemove);
280 let absent = failed.transition(Event::AbsenceVerified).unwrap();
281 assert!(!absent.blocks_acquisition());
282 assert_eq!(absent.recover().unwrap(), absent);
283 }
284 }
285 #[test]
286 fn present_reconciliation_requires_new_operator_authorization() {
287 for phase in [Phase::Removing, Phase::AwaitingAbsence] {
288 let state = State {
289 phase,
290 object_id: Some("e".repeat(64)),
291 }
292 .recover()
293 .unwrap();
294 assert!(state.transition(Event::BeginRemove).is_err());
295 assert!(state.transition(Event::CleanupRunningVerified).is_err());
296 let stopped = state
297 .transition(Event::OperatorCleanupRunningVerified)
298 .unwrap()
299 .transition(Event::StoppedVerified)
300 .unwrap();
301 let absent = stopped
302 .transition(Event::BeginRemove)
303 .unwrap()
304 .transition(Event::RemoveClaimed)
305 .unwrap()
306 .transition(Event::AbsenceVerified)
307 .unwrap();
308 assert!(!absent.blocks_acquisition());
309 assert_eq!(absent.object_id(), state.object_id());
310 let terminal = state
311 .transition(Event::OperatorCleanupTerminalVerified)
312 .unwrap();
313 assert_eq!(terminal.phase(), Phase::Stopped);
314 }
315 }
316
317 #[test]
318 fn retained_absence_needs_operator_evidence_and_never_a_remove_claim() {
319 let retained = created().recover().unwrap();
320 assert!(retained.transition(Event::AbsenceVerified).is_err());
321 assert!(retained.transition(Event::RemoveClaimed).is_err());
322 let absent = retained.transition(Event::OperatorAbsenceVerified).unwrap();
323 assert!(!absent.blocks_acquisition());
324 assert!(State::pending_create()
325 .transition(Event::OperatorAbsenceVerified)
326 .is_err());
327 assert!(State::pending_create()
328 .recover()
329 .unwrap()
330 .transition(Event::OperatorAbsenceVerified)
331 .is_err());
332 }
333}