Skip to main content

muster/domain/process/
exit_intent.rs

1/// Desired handling of the current child process when it exits.
2#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
3pub enum ExitIntent {
4    /// Apply the configured restart policy to an ordinary exit.
5    #[default]
6    FollowPolicy,
7    /// Keep the process stopped, but permit another signal attempt because no
8    /// successful delivery is currently in flight.
9    StopRetryable,
10    /// Keep the process stopped and reuse the successfully delivered request.
11    StopInFlight,
12    /// Restart after exit, but permit another signal attempt because no
13    /// successful delivery is currently in flight.
14    RestartRetryable,
15    /// Restart after exit and reuse the successfully delivered request.
16    RestartInFlight,
17}
18
19impl ExitIntent {
20    /// Records explicit stop intent before attempting signal delivery.
21    pub fn request_stop(self) -> Self {
22        Self::StopRetryable
23    }
24
25    /// Records that a stop signal was delivered successfully.
26    pub fn stop_delivered(self) -> Self {
27        if self.is_stop() {
28            Self::StopInFlight
29        } else {
30            self
31        }
32    }
33
34    /// Records failed stop delivery, permitting a later retry while preserving
35    /// the user's instruction not to restart when the child exits.
36    pub fn stop_delivery_failed(self) -> Self {
37        if self.is_stop() {
38            Self::StopRetryable
39        } else {
40            self
41        }
42    }
43
44    /// Replaces any prior exit intent with an explicit restart request.
45    pub fn request_restart(self) -> Self {
46        Self::RestartRetryable
47    }
48
49    /// Records that a restart signal was delivered successfully.
50    pub fn restart_delivered(self) -> Self {
51        if self.is_restart() {
52            Self::RestartInFlight
53        } else {
54            self
55        }
56    }
57
58    /// Records failed restart delivery, permitting a later retry while
59    /// preserving the user's instruction to restart after any eventual exit.
60    pub fn restart_delivery_failed(self) -> Self {
61        if self.is_restart() {
62            Self::RestartRetryable
63        } else {
64            self
65        }
66    }
67
68    /// Whether another stop input should attempt signal delivery.
69    pub fn accepts_stop_request(self) -> bool {
70        self != Self::StopInFlight
71    }
72
73    /// Whether another restart input should attempt signal delivery.
74    pub fn accepts_restart_request(self) -> bool {
75        self != Self::RestartInFlight
76    }
77
78    /// Whether this intent represents an explicit request to remain stopped.
79    pub fn is_stop(self) -> bool {
80        matches!(self, Self::StopRetryable | Self::StopInFlight)
81    }
82
83    /// Whether this intent represents an explicit restart request.
84    pub fn is_restart(self) -> bool {
85        matches!(self, Self::RestartRetryable | Self::RestartInFlight)
86    }
87
88    /// Records that hard-kill escalation failed, preserving the requested exit
89    /// behavior while permitting another signal attempt.
90    pub fn force_stop_delivery_failed(self) -> Self {
91        match self {
92            Self::StopInFlight => Self::StopRetryable,
93            Self::RestartInFlight => Self::RestartRetryable,
94            _ => self,
95        }
96    }
97
98    /// Whether a graceful stop is awaiting hard-kill escalation.
99    pub fn awaits_force_stop(self) -> bool {
100        matches!(self, Self::StopInFlight | Self::RestartInFlight)
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn failed_delivery_preserves_stop_intent_and_allows_retry() {
110        let intent = ExitIntent::default().request_stop().stop_delivery_failed();
111
112        assert!(intent.is_stop());
113        assert!(intent.accepts_stop_request());
114        assert!(!intent.awaits_force_stop());
115    }
116
117    #[test]
118    fn successful_delivery_suppresses_duplicate_stop_requests() {
119        let intent = ExitIntent::default().request_stop().stop_delivered();
120
121        assert!(intent.is_stop());
122        assert!(!intent.accepts_stop_request());
123        assert!(intent.awaits_force_stop());
124    }
125
126    #[test]
127    fn restart_replaces_a_prior_stop_intent() {
128        let intent = ExitIntent::default()
129            .request_stop()
130            .stop_delivered()
131            .request_restart();
132
133        assert_eq!(intent, ExitIntent::RestartRetryable);
134        assert!(!intent.is_stop());
135        assert!(intent.is_restart());
136    }
137
138    /// A delivered restart waits for exit and permits timeout escalation.
139    #[test]
140    fn successful_restart_delivery_awaits_force_stop() {
141        let intent = ExitIntent::default().request_restart().restart_delivered();
142
143        assert_eq!(intent, ExitIntent::RestartInFlight);
144        assert!(!intent.accepts_restart_request());
145        assert!(intent.awaits_force_stop());
146    }
147
148    /// Failed escalation keeps the user's restart intent retryable.
149    #[test]
150    fn failed_restart_escalation_remains_retryable() {
151        let intent = ExitIntent::RestartInFlight.force_stop_delivery_failed();
152
153        assert_eq!(intent, ExitIntent::RestartRetryable);
154        assert!(intent.accepts_restart_request());
155    }
156}