1#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum ModelState {
10 Unloaded,
11 Loading,
12 Loaded,
13 Unloading,
14 Failed { reason: String },
15}
16
17impl ModelState {
18 pub fn name(&self) -> &'static str {
20 match self {
21 Self::Unloaded => "unloaded",
22 Self::Loading => "loading",
23 Self::Loaded => "loaded",
24 Self::Unloading => "unloading",
25 Self::Failed { .. } => "failed",
26 }
27 }
28
29 pub fn serves(&self) -> bool {
31 matches!(self, Self::Loaded)
32 }
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum Command {
38 None,
39 BeginLoad,
40 BeginUnload,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
45#[error("{event} is unexpected while {state}")]
46pub struct UnexpectedEvent {
47 pub event: &'static str,
48 pub state: &'static str,
49}
50
51#[derive(Debug, Clone)]
53pub struct Lifecycle {
54 state: ModelState,
55 unload_pending: bool,
57}
58
59impl Default for Lifecycle {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65impl Lifecycle {
66 pub fn new() -> Self {
67 Self {
68 state: ModelState::Unloaded,
69 unload_pending: false,
70 }
71 }
72
73 pub fn state(&self) -> &ModelState {
74 &self.state
75 }
76
77 pub fn request_load(&mut self) -> Command {
78 match self.state {
79 ModelState::Unloaded | ModelState::Failed { .. } => {
80 self.state = ModelState::Loading;
81 Command::BeginLoad
82 }
83 ModelState::Loading => {
84 self.unload_pending = false;
85 Command::None
86 }
87 ModelState::Loaded | ModelState::Unloading => Command::None,
88 }
89 }
90
91 pub fn request_unload(&mut self) -> Command {
92 match self.state {
93 ModelState::Loaded => {
94 self.state = ModelState::Unloading;
95 Command::BeginUnload
96 }
97 ModelState::Loading => {
98 self.unload_pending = true;
99 Command::None
100 }
101 ModelState::Failed { .. } => {
102 self.state = ModelState::Unloaded;
103 Command::None
104 }
105 ModelState::Unloaded | ModelState::Unloading => Command::None,
106 }
107 }
108
109 pub fn load_finished(
110 &mut self,
111 outcome: Result<(), String>,
112 ) -> Result<Command, UnexpectedEvent> {
113 if self.state != ModelState::Loading {
114 return Err(self.unexpected("load_finished"));
115 }
116 let unload_pending = std::mem::take(&mut self.unload_pending);
117 match outcome {
118 Ok(()) if unload_pending => {
119 self.state = ModelState::Unloading;
120 Ok(Command::BeginUnload)
121 }
122 Ok(()) => {
123 self.state = ModelState::Loaded;
124 Ok(Command::None)
125 }
126 Err(reason) => {
127 self.state = ModelState::Failed { reason };
128 Ok(Command::None)
129 }
130 }
131 }
132
133 pub fn unload_finished(
134 &mut self,
135 outcome: Result<(), String>,
136 ) -> Result<Command, UnexpectedEvent> {
137 if self.state != ModelState::Unloading {
138 return Err(self.unexpected("unload_finished"));
139 }
140 self.state = match outcome {
141 Ok(()) => ModelState::Unloaded,
142 Err(reason) => ModelState::Failed { reason },
143 };
144 Ok(Command::None)
145 }
146
147 fn unexpected(&self, event: &'static str) -> UnexpectedEvent {
148 UnexpectedEvent {
149 event,
150 state: self.state.name(),
151 }
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 fn loaded() -> Lifecycle {
160 let mut l = Lifecycle::new();
161 assert_eq!(l.request_load(), Command::BeginLoad);
162 assert_eq!(l.load_finished(Ok(())), Ok(Command::None));
163 l
164 }
165
166 #[test]
167 fn starts_unloaded() {
168 assert_eq!(Lifecycle::new().state(), &ModelState::Unloaded);
169 }
170
171 #[test]
172 fn load_moves_unloaded_to_loading_then_loaded() {
173 let mut l = Lifecycle::new();
174 assert_eq!(l.request_load(), Command::BeginLoad);
175 assert_eq!(l.state(), &ModelState::Loading);
176 assert_eq!(l.load_finished(Ok(())), Ok(Command::None));
177 assert_eq!(l.state(), &ModelState::Loaded);
178 }
179
180 #[test]
181 fn load_failure_moves_to_failed_with_the_reason() {
182 let mut l = Lifecycle::new();
183 l.request_load();
184 assert_eq!(l.load_finished(Err("cuda oom".into())), Ok(Command::None));
185 assert_eq!(
186 l.state(),
187 &ModelState::Failed {
188 reason: "cuda oom".into()
189 }
190 );
191 }
192
193 #[test]
194 fn load_is_a_noop_while_loading_or_loaded() {
195 let mut l = Lifecycle::new();
196 l.request_load();
197 assert_eq!(l.request_load(), Command::None);
198 assert_eq!(l.state(), &ModelState::Loading);
199 let mut l = loaded();
200 assert_eq!(l.request_load(), Command::None);
201 assert_eq!(l.state(), &ModelState::Loaded);
202 }
203
204 #[test]
205 fn load_retries_from_failed() {
206 let mut l = Lifecycle::new();
207 l.request_load();
208 l.load_finished(Err("x".into())).unwrap();
209 assert_eq!(l.request_load(), Command::BeginLoad);
210 assert_eq!(l.state(), &ModelState::Loading);
211 }
212
213 #[test]
214 fn unload_moves_loaded_to_unloading_then_unloaded() {
215 let mut l = loaded();
216 assert_eq!(l.request_unload(), Command::BeginUnload);
217 assert_eq!(l.state(), &ModelState::Unloading);
218 assert_eq!(l.unload_finished(Ok(())), Ok(Command::None));
219 assert_eq!(l.state(), &ModelState::Unloaded);
220 }
221
222 #[test]
223 fn unload_failure_moves_to_failed() {
224 let mut l = loaded();
225 l.request_unload();
226 l.unload_finished(Err("stuck".into())).unwrap();
227 assert_eq!(
228 l.state(),
229 &ModelState::Failed {
230 reason: "stuck".into()
231 }
232 );
233 }
234
235 #[test]
236 fn unload_is_a_noop_while_unloaded_or_unloading() {
237 let mut l = Lifecycle::new();
238 assert_eq!(l.request_unload(), Command::None);
239 assert_eq!(l.state(), &ModelState::Unloaded);
240 let mut l = loaded();
241 l.request_unload();
242 assert_eq!(l.request_unload(), Command::None);
243 assert_eq!(l.state(), &ModelState::Unloading);
244 }
245
246 #[test]
247 fn unload_clears_a_failed_model() {
248 let mut l = Lifecycle::new();
249 l.request_load();
250 l.load_finished(Err("x".into())).unwrap();
251 assert_eq!(l.request_unload(), Command::None);
252 assert_eq!(l.state(), &ModelState::Unloaded);
253 }
254
255 #[test]
256 fn unload_while_loading_runs_after_the_load_succeeds() {
257 let mut l = Lifecycle::new();
258 l.request_load();
259 assert_eq!(l.request_unload(), Command::None);
260 assert_eq!(l.state(), &ModelState::Loading);
261 assert_eq!(l.load_finished(Ok(())), Ok(Command::BeginUnload));
262 assert_eq!(l.state(), &ModelState::Unloading);
263 }
264
265 #[test]
266 fn unload_while_loading_is_dropped_when_the_load_fails() {
267 let mut l = Lifecycle::new();
268 l.request_load();
269 l.request_unload();
270 assert_eq!(l.load_finished(Err("x".into())), Ok(Command::None));
271 assert!(matches!(l.state(), ModelState::Failed { .. }));
272 }
273
274 #[test]
275 fn load_after_a_pending_unload_cancels_it() {
276 let mut l = Lifecycle::new();
277 l.request_load();
278 l.request_unload();
279 assert_eq!(l.request_load(), Command::None);
280 assert_eq!(l.load_finished(Ok(())), Ok(Command::None));
281 assert_eq!(l.state(), &ModelState::Loaded);
282 }
283
284 #[test]
285 fn a_finish_event_in_the_wrong_state_is_refused() {
286 let mut l = Lifecycle::new();
287 assert_eq!(
288 l.load_finished(Ok(())),
289 Err(UnexpectedEvent {
290 event: "load_finished",
291 state: "unloaded"
292 })
293 );
294 assert_eq!(
295 l.unload_finished(Ok(())),
296 Err(UnexpectedEvent {
297 event: "unload_finished",
298 state: "unloaded"
299 })
300 );
301 assert_eq!(l.state(), &ModelState::Unloaded);
302 }
303
304 #[test]
305 fn state_names_are_the_wire_names() {
306 assert_eq!(ModelState::Unloaded.name(), "unloaded");
307 assert_eq!(ModelState::Loading.name(), "loading");
308 assert_eq!(ModelState::Loaded.name(), "loaded");
309 assert_eq!(ModelState::Unloading.name(), "unloading");
310 assert_eq!(ModelState::Failed { reason: "r".into() }.name(), "failed");
311 }
312
313 #[test]
314 fn only_loaded_serves() {
315 assert!(ModelState::Loaded.serves());
316 for s in [
317 ModelState::Unloaded,
318 ModelState::Loading,
319 ModelState::Unloading,
320 ModelState::Failed { reason: "r".into() },
321 ] {
322 assert!(!s.serves(), "{} must not serve", s.name());
323 }
324 }
325
326 #[test]
327 fn unexpected_event_displays_event_and_state() {
328 let e = UnexpectedEvent {
329 event: "load_finished",
330 state: "loaded",
331 };
332 assert_eq!(e.to_string(), "load_finished is unexpected while loaded");
333 }
334}