ghidra_worker_ctl/
state.rs1use thiserror::Error;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum WorkerState {
10 Starting,
11 Ready,
12 Busy,
13 Crashed,
14}
15
16#[derive(Debug, Error, PartialEq, Eq)]
17#[error("invalid worker transition from {from:?}: {event}")]
18pub struct InvalidTransition {
19 pub from: WorkerState,
20 pub event: &'static str,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct WorkerLifecycle {
28 state: WorkerState,
29 restart_notice_pending: bool,
30 ever_ready: bool,
33}
34
35impl Default for WorkerLifecycle {
36 fn default() -> Self {
37 Self {
38 state: WorkerState::Starting,
39 restart_notice_pending: false,
40 ever_ready: false,
41 }
42 }
43}
44
45impl WorkerLifecycle {
46 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn state(&self) -> WorkerState {
51 self.state
52 }
53
54 pub fn on_ready(&mut self) -> Result<(), InvalidTransition> {
56 match self.state {
57 WorkerState::Starting => {
58 self.state = WorkerState::Ready;
59 self.ever_ready = true;
60 Ok(())
61 }
62 from => Err(InvalidTransition {
63 from,
64 event: "on_ready",
65 }),
66 }
67 }
68
69 pub fn on_dispatch(&mut self) -> Result<(), InvalidTransition> {
71 match self.state {
72 WorkerState::Ready => {
73 self.state = WorkerState::Busy;
74 Ok(())
75 }
76 from => Err(InvalidTransition {
77 from,
78 event: "on_dispatch",
79 }),
80 }
81 }
82
83 pub fn on_complete(&mut self) -> Result<(), InvalidTransition> {
85 match self.state {
86 WorkerState::Busy => {
87 self.state = WorkerState::Ready;
88 Ok(())
89 }
90 from => Err(InvalidTransition {
91 from,
92 event: "on_complete",
93 }),
94 }
95 }
96
97 pub fn on_crash(&mut self) {
99 self.state = WorkerState::Crashed;
100 }
101
102 pub fn on_respawn(&mut self) -> Result<(), InvalidTransition> {
104 match self.state {
105 WorkerState::Crashed => {
106 self.state = WorkerState::Starting;
107 self.restart_notice_pending = self.ever_ready;
108 Ok(())
109 }
110 from => Err(InvalidTransition {
111 from,
112 event: "on_respawn",
113 }),
114 }
115 }
116
117 pub fn take_restart_notice(&mut self) -> bool {
119 std::mem::take(&mut self.restart_notice_pending)
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn starts_in_starting() {
129 assert_eq!(WorkerLifecycle::new().state(), WorkerState::Starting);
130 }
131
132 #[test]
133 fn happy_path_starting_ready_busy_ready() {
134 let mut w = WorkerLifecycle::new();
135 w.on_ready().unwrap();
136 assert_eq!(w.state(), WorkerState::Ready);
137 w.on_dispatch().unwrap();
138 assert_eq!(w.state(), WorkerState::Busy);
139 w.on_complete().unwrap();
140 assert_eq!(w.state(), WorkerState::Ready);
141 }
142
143 #[test]
144 fn cannot_dispatch_before_ready() {
145 let mut w = WorkerLifecycle::new();
146 assert_eq!(
147 w.on_dispatch(),
148 Err(InvalidTransition {
149 from: WorkerState::Starting,
150 event: "on_dispatch"
151 })
152 );
153 }
154
155 #[test]
156 fn cannot_ready_twice() {
157 let mut w = WorkerLifecycle::new();
158 w.on_ready().unwrap();
159 assert!(w.on_ready().is_err());
160 }
161
162 #[test]
163 fn crash_then_respawn_arms_restart_notice_exactly_once() {
164 let mut w = WorkerLifecycle::new();
165 w.on_ready().unwrap();
166 w.on_dispatch().unwrap();
167 w.on_crash(); assert_eq!(w.state(), WorkerState::Crashed);
169 w.on_respawn().unwrap();
170 assert_eq!(w.state(), WorkerState::Starting);
171 assert!(w.take_restart_notice());
173 assert!(!w.take_restart_notice());
174 }
175
176 #[test]
177 fn no_restart_notice_before_any_crash() {
178 let mut w = WorkerLifecycle::new();
179 assert!(!w.take_restart_notice());
180 }
181
182 #[test]
183 fn cannot_respawn_a_live_worker() {
184 let mut w = WorkerLifecycle::new();
185 w.on_ready().unwrap();
186 assert_eq!(
187 w.on_respawn(),
188 Err(InvalidTransition {
189 from: WorkerState::Ready,
190 event: "on_respawn"
191 })
192 );
193 }
194
195 #[test]
196 fn crash_before_first_ready_does_not_arm_restart_notice() {
197 let mut w = WorkerLifecycle::new(); w.on_crash();
199 w.on_respawn().unwrap();
200 assert!(!w.take_restart_notice());
202 }
203}