oris_kernel/kernel/
execution_suspension.rs1use serde::{Deserialize, Serialize};
9
10use crate::kernel::identity::RunId;
11
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
14pub enum ExecutionSuspensionState {
15 Running,
17 Suspended,
19 WaitingInput,
21}
22
23impl Default for ExecutionSuspensionState {
24 fn default() -> Self {
25 ExecutionSuspensionState::Running
26 }
27}
28
29#[derive(Clone, Debug, Serialize, Deserialize)]
31pub struct ExecutionSuspension {
32 pub run_id: RunId,
34 pub state: ExecutionSuspensionState,
36 pub state_changed_at: chrono::DateTime<chrono::Utc>,
38 pub reason: Option<String>,
40}
41
42impl ExecutionSuspension {
43 pub fn new(run_id: RunId) -> Self {
45 Self {
46 run_id,
47 state: ExecutionSuspensionState::Running,
48 state_changed_at: chrono::Utc::now(),
49 reason: None,
50 }
51 }
52
53 pub fn suspend(&mut self, reason: Option<String>) -> Result<(), SuspensionError> {
55 if self.state != ExecutionSuspensionState::Running {
56 return Err(SuspensionError::InvalidTransition {
57 from: self.state.clone(),
58 to: "Suspended".into(),
59 });
60 }
61 self.state = ExecutionSuspensionState::Suspended;
62 self.state_changed_at = chrono::Utc::now();
63 self.reason = reason;
64 Ok(())
65 }
66
67 pub fn wait_input(&mut self) -> Result<(), SuspensionError> {
69 if self.state != ExecutionSuspensionState::Suspended {
70 return Err(SuspensionError::InvalidTransition {
71 from: self.state.clone(),
72 to: "WaitingInput".into(),
73 });
74 }
75 self.state = ExecutionSuspensionState::WaitingInput;
76 self.state_changed_at = chrono::Utc::now();
77 Ok(())
78 }
79
80 pub fn resume(&mut self) -> Result<(), SuspensionError> {
82 if self.state != ExecutionSuspensionState::WaitingInput {
83 return Err(SuspensionError::InvalidTransition {
84 from: self.state.clone(),
85 to: "Running".into(),
86 });
87 }
88 self.state = ExecutionSuspensionState::Running;
89 self.state_changed_at = chrono::Utc::now();
90 self.reason = None;
91 Ok(())
92 }
93
94 pub fn is_running(&self) -> bool {
96 self.state == ExecutionSuspensionState::Running
97 }
98
99 pub fn is_suspended(&self) -> bool {
101 matches!(
102 self.state,
103 ExecutionSuspensionState::Suspended | ExecutionSuspensionState::WaitingInput
104 )
105 }
106}
107
108#[derive(Debug, thiserror::Error)]
110pub enum SuspensionError {
111 #[error("Invalid state transition from {from:?} to {to}")]
112 InvalidTransition {
113 from: ExecutionSuspensionState,
114 to: String,
115 },
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn new_suspension_is_running() {
124 let susp = ExecutionSuspension::new("run-1".into());
125 assert!(susp.is_running());
126 assert!(!susp.is_suspended());
127 }
128
129 #[test]
130 fn running_to_suspended_transition() {
131 let mut susp = ExecutionSuspension::new("run-1".into());
132 susp.suspend(Some("user requested".into())).unwrap();
133 assert!(!susp.is_running());
134 assert!(susp.is_suspended());
135 assert_eq!(susp.state, ExecutionSuspensionState::Suspended);
136 }
137
138 #[test]
139 fn suspended_to_waiting_input() {
140 let mut susp = ExecutionSuspension::new("run-1".into());
141 susp.suspend(None).unwrap();
142 susp.wait_input().unwrap();
143 assert_eq!(susp.state, ExecutionSuspensionState::WaitingInput);
144 }
145
146 #[test]
147 fn waiting_input_to_running_resume() {
148 let mut susp = ExecutionSuspension::new("run-1".into());
149 susp.suspend(None).unwrap();
150 susp.wait_input().unwrap();
151 susp.resume().unwrap();
152 assert!(susp.is_running());
153 }
154
155 #[test]
156 fn invalid_transition_running_to_waiting() {
157 let mut susp = ExecutionSuspension::new("run-1".into());
158 let err = susp.wait_input().unwrap_err();
159 println!("Error: {:?}", err);
160 assert!(err.to_string().contains("Invalid state transition"));
161 }
162}