Skip to main content

behavior/supervision/domain/
incarnation.rs

1//! Pure lifecycle domain for one stable proxy's worker incarnation.
2
3use crate::{CreationKind, CreationRejection, CreationResolved};
4
5/// The complete lifecycle state of the worker behind one stable proxy.
6enum IncarnationState<N, C> {
7    Dormant {
8        initial: C,
9    },
10    Installing {
11        attempt: N,
12        kind: CreationKind<N>,
13    },
14    Running {
15        incarnation: N,
16        queued_replacement: Option<C>,
17    },
18    Vacant {
19        last_installed: Option<N>,
20    },
21}
22
23/// A copyable observation of the lifecycle without owned child specifications.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum IncarnationPhase<N> {
26    Dormant,
27    Installing { attempt: N, kind: CreationKind<N> },
28    Running { incarnation: N },
29    AwaitingStop { incarnation: N },
30    Vacant { last_installed: Option<N> },
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
34pub(crate) enum IncarnationError {
35    #[error("the incarnation lifecycle was already initialized")]
36    AlreadyInitialized,
37    #[error("the incarnation creation-attempt sequence is exhausted")]
38    AttemptSequenceExhausted,
39}
40
41/// A fresh child creation selected by the lifecycle transition.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub(crate) struct IncarnationCreation<N, C> {
44    pub attempt: N,
45    pub kind: CreationKind<N>,
46    pub child: C,
47}
48
49impl<N, C> IncarnationCreation<N, C> {
50    #[must_use]
51    pub const fn new(attempt: N, kind: CreationKind<N>, child: C) -> Self {
52        Self {
53            attempt,
54            kind,
55            child,
56        }
57    }
58}
59
60/// Independent effects selected by one lifecycle transition.
61///
62/// `creation` and `report` are independent because accepting an exact stop
63/// can both report that stop and begin an already-queued replacement.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub(crate) struct IncarnationEffects<N, C, M> {
66    pub creation: Option<IncarnationCreation<N, C>>,
67    pub delivery: Option<(N, M)>,
68    pub creation_report: Option<CreationResolved<N>>,
69}
70
71/// Effects of accepting an exact child-stop observation.
72///
73/// This is distinct from [`IncarnationEffects`] so the adapter never has to
74/// reconstruct stop provenance from an optional, unrelated input.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub(crate) struct IncarnationStopEffects<N, C> {
77    pub creation: Option<IncarnationCreation<N, C>>,
78    pub stopped: Option<N>,
79}
80
81impl<N, C, M> IncarnationEffects<N, C, M> {
82    #[must_use]
83    pub fn new(
84        creation: Option<IncarnationCreation<N, C>>,
85        delivery: Option<(N, M)>,
86        creation_report: Option<CreationResolved<N>>,
87    ) -> Self {
88        Self {
89            creation,
90            delivery,
91            creation_report,
92        }
93    }
94
95    #[must_use]
96    pub fn none() -> Self {
97        Self {
98            creation: None,
99            delivery: None,
100            creation_report: None,
101        }
102    }
103}
104
105/// The typed state machine for one proxy's sequence of fresh incarnations.
106pub(crate) struct Incarnation<N, C> {
107    state: IncarnationState<N, C>,
108    next_attempt: u64,
109}
110
111impl<N, C> Incarnation<N, C> {
112    #[must_use]
113    pub const fn new(initial: C) -> Self {
114        Self {
115            state: IncarnationState::Dormant { initial },
116            next_attempt: 0,
117        }
118    }
119}
120
121impl<N: Copy, C> Incarnation<N, C> {
122    #[must_use]
123    pub const fn phase(&self) -> IncarnationPhase<N> {
124        match &self.state {
125            IncarnationState::Dormant { .. } => IncarnationPhase::Dormant,
126            IncarnationState::Installing { attempt, kind } => IncarnationPhase::Installing {
127                attempt: *attempt,
128                kind: *kind,
129            },
130            IncarnationState::Running {
131                incarnation,
132                queued_replacement: Some(_),
133            } => IncarnationPhase::AwaitingStop {
134                incarnation: *incarnation,
135            },
136            IncarnationState::Running {
137                incarnation,
138                queued_replacement: None,
139            } => IncarnationPhase::Running {
140                incarnation: *incarnation,
141            },
142            IncarnationState::Vacant { last_installed } => IncarnationPhase::Vacant {
143                last_installed: *last_installed,
144            },
145        }
146    }
147}
148
149impl<N: Copy + From<u64> + PartialEq, C> Incarnation<N, C> {
150    /// Emit the initial fresh creation exactly once.
151    ///
152    /// # Errors
153    /// Returns [`IncarnationError::AlreadyInitialized`] after leaving
154    /// `Dormant`.
155    pub(crate) fn initialize<M>(
156        &mut self,
157    ) -> Result<IncarnationEffects<N, C, M>, IncarnationError> {
158        let previous = core::mem::replace(
159            &mut self.state,
160            IncarnationState::Vacant {
161                last_installed: None,
162            },
163        );
164        match previous {
165            IncarnationState::Dormant { initial } => self.begin(initial, CreationKind::Birth),
166            state => {
167                self.state = state;
168                Err(IncarnationError::AlreadyInitialized)
169            }
170        }
171    }
172
173    fn begin<M>(
174        &mut self,
175        child: C,
176        kind: CreationKind<N>,
177    ) -> Result<IncarnationEffects<N, C, M>, IncarnationError> {
178        let attempt = N::from(self.next_attempt);
179        self.next_attempt = self
180            .next_attempt
181            .checked_add(1)
182            .ok_or(IncarnationError::AttemptSequenceExhausted)?;
183        self.state = IncarnationState::Installing { attempt, kind };
184        Ok(IncarnationEffects::new(
185            Some(IncarnationCreation::new(attempt, kind, child)),
186            None,
187            None,
188        ))
189    }
190
191    pub(crate) fn creation_resolved<M>(
192        &mut self,
193        attempt: N,
194        kind: CreationKind<N>,
195        result: Result<(), CreationRejection>,
196    ) -> IncarnationEffects<N, C, M> {
197        let IncarnationState::Installing {
198            attempt: pending,
199            kind: pending_kind,
200        } = self.state
201        else {
202            return IncarnationEffects::none();
203        };
204        if attempt != pending || kind != pending_kind {
205            return IncarnationEffects::none();
206        }
207        self.state = match result {
208            Ok(()) => IncarnationState::Running {
209                incarnation: attempt,
210                queued_replacement: None,
211            },
212            Err(_) => IncarnationState::Vacant {
213                last_installed: match kind {
214                    CreationKind::Birth => None,
215                    CreationKind::ReplacementIncarnation { replaces } => Some(replaces),
216                },
217            },
218        };
219        IncarnationEffects::new(
220            None,
221            None,
222            Some(CreationResolved::new(attempt, kind, result)),
223        )
224    }
225
226    pub(crate) fn child_stopped(
227        &mut self,
228        stopped: N,
229    ) -> Result<IncarnationStopEffects<N, C>, IncarnationError> {
230        let IncarnationState::Running { incarnation, .. } = self.state else {
231            return Ok(IncarnationStopEffects {
232                creation: None,
233                stopped: None,
234            });
235        };
236        if stopped != incarnation {
237            return Ok(IncarnationStopEffects {
238                creation: None,
239                stopped: None,
240            });
241        }
242        let previous = core::mem::replace(
243            &mut self.state,
244            IncarnationState::Vacant {
245                last_installed: Some(incarnation),
246            },
247        );
248        let IncarnationState::Running {
249            incarnation,
250            queued_replacement,
251        } = previous
252        else {
253            self.state = previous;
254            return Ok(IncarnationStopEffects {
255                creation: None,
256                stopped: None,
257            });
258        };
259        let creation = match queued_replacement {
260            Some(child) => {
261                self.begin::<()>(
262                    child,
263                    CreationKind::ReplacementIncarnation {
264                        replaces: incarnation,
265                    },
266                )?
267                .creation
268            }
269            None => None,
270        };
271        Ok(IncarnationStopEffects {
272            creation,
273            stopped: Some(incarnation),
274        })
275    }
276
277    pub(crate) fn forward<M>(&self, message: M) -> IncarnationEffects<N, C, M> {
278        let delivery = match self.state {
279            IncarnationState::Running { incarnation, .. } => Some((incarnation, message)),
280            IncarnationState::Dormant { .. }
281            | IncarnationState::Installing { .. }
282            | IncarnationState::Vacant { .. } => None,
283        };
284        IncarnationEffects::new(None, delivery, None)
285    }
286
287    pub(crate) fn replace<M>(
288        &mut self,
289        child: C,
290    ) -> Result<IncarnationEffects<N, C, M>, IncarnationError> {
291        Ok(match &mut self.state {
292            IncarnationState::Running {
293                queued_replacement: queued_replacement @ None,
294                ..
295            } => {
296                *queued_replacement = Some(child);
297                IncarnationEffects::none()
298            }
299            IncarnationState::Vacant {
300                last_installed: Some(last),
301            } => {
302                let replaces = *last;
303                return self.begin(child, CreationKind::ReplacementIncarnation { replaces });
304            }
305            IncarnationState::Dormant { .. }
306            | IncarnationState::Installing { .. }
307            | IncarnationState::Running { .. }
308            | IncarnationState::Vacant {
309                last_installed: None,
310            } => IncarnationEffects::none(),
311        })
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn rejected_attempt_preserves_successful_provenance() {
321        let mut machine = Incarnation::<u64, &'static str>::new("first");
322        machine.initialize::<()>().unwrap();
323        machine.creation_resolved::<()>(0, CreationKind::Birth, Ok(()));
324        machine.replace::<()>("second").unwrap();
325        machine.child_stopped(0).unwrap();
326        machine.creation_resolved::<()>(
327            1,
328            CreationKind::ReplacementIncarnation { replaces: 0 },
329            Err(CreationRejection::EnvironmentFailed),
330        );
331
332        assert_eq!(
333            machine.phase(),
334            IncarnationPhase::Vacant {
335                last_installed: Some(0)
336            }
337        );
338        let effects = machine.replace::<()>("third").unwrap();
339        let creation = effects.creation.expect("replacement begins");
340        assert_eq!(creation.attempt, 2);
341        assert_eq!(
342            creation.kind,
343            CreationKind::ReplacementIncarnation { replaces: 0 }
344        );
345    }
346
347    #[test]
348    fn stale_inputs_are_inert() {
349        let mut machine = Incarnation::<u64, ()>::new(());
350        machine.initialize::<()>().unwrap();
351        let effects = machine.creation_resolved::<()>(9, CreationKind::Birth, Ok(()));
352        assert!(effects.creation.is_none());
353        assert!(effects.delivery.is_none());
354        assert!(effects.creation_report.is_none());
355        assert_eq!(
356            machine.phase(),
357            IncarnationPhase::Installing {
358                attempt: 0,
359                kind: CreationKind::Birth
360            }
361        );
362    }
363}