use serde::{Deserialize, Serialize};
use crate::kernel::identity::RunId;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ExecutionSuspensionState {
Running,
Suspended,
WaitingInput,
}
impl Default for ExecutionSuspensionState {
fn default() -> Self {
ExecutionSuspensionState::Running
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExecutionSuspension {
pub run_id: RunId,
pub state: ExecutionSuspensionState,
pub state_changed_at: chrono::DateTime<chrono::Utc>,
pub reason: Option<String>,
}
impl ExecutionSuspension {
pub fn new(run_id: RunId) -> Self {
Self {
run_id,
state: ExecutionSuspensionState::Running,
state_changed_at: chrono::Utc::now(),
reason: None,
}
}
pub fn suspend(&mut self, reason: Option<String>) -> Result<(), SuspensionError> {
if self.state != ExecutionSuspensionState::Running {
return Err(SuspensionError::InvalidTransition {
from: self.state.clone(),
to: "Suspended".into(),
});
}
self.state = ExecutionSuspensionState::Suspended;
self.state_changed_at = chrono::Utc::now();
self.reason = reason;
Ok(())
}
pub fn wait_input(&mut self) -> Result<(), SuspensionError> {
if self.state != ExecutionSuspensionState::Suspended {
return Err(SuspensionError::InvalidTransition {
from: self.state.clone(),
to: "WaitingInput".into(),
});
}
self.state = ExecutionSuspensionState::WaitingInput;
self.state_changed_at = chrono::Utc::now();
Ok(())
}
pub fn resume(&mut self) -> Result<(), SuspensionError> {
if self.state != ExecutionSuspensionState::WaitingInput {
return Err(SuspensionError::InvalidTransition {
from: self.state.clone(),
to: "Running".into(),
});
}
self.state = ExecutionSuspensionState::Running;
self.state_changed_at = chrono::Utc::now();
self.reason = None;
Ok(())
}
pub fn is_running(&self) -> bool {
self.state == ExecutionSuspensionState::Running
}
pub fn is_suspended(&self) -> bool {
matches!(
self.state,
ExecutionSuspensionState::Suspended | ExecutionSuspensionState::WaitingInput
)
}
}
#[derive(Debug, thiserror::Error)]
pub enum SuspensionError {
#[error("Invalid state transition from {from:?} to {to}")]
InvalidTransition {
from: ExecutionSuspensionState,
to: String,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_suspension_is_running() {
let susp = ExecutionSuspension::new("run-1".into());
assert!(susp.is_running());
assert!(!susp.is_suspended());
}
#[test]
fn running_to_suspended_transition() {
let mut susp = ExecutionSuspension::new("run-1".into());
susp.suspend(Some("user requested".into())).unwrap();
assert!(!susp.is_running());
assert!(susp.is_suspended());
assert_eq!(susp.state, ExecutionSuspensionState::Suspended);
}
#[test]
fn suspended_to_waiting_input() {
let mut susp = ExecutionSuspension::new("run-1".into());
susp.suspend(None).unwrap();
susp.wait_input().unwrap();
assert_eq!(susp.state, ExecutionSuspensionState::WaitingInput);
}
#[test]
fn waiting_input_to_running_resume() {
let mut susp = ExecutionSuspension::new("run-1".into());
susp.suspend(None).unwrap();
susp.wait_input().unwrap();
susp.resume().unwrap();
assert!(susp.is_running());
}
#[test]
fn invalid_transition_running_to_waiting() {
let mut susp = ExecutionSuspension::new("run-1".into());
let err = susp.wait_input().unwrap_err();
println!("Error: {:?}", err);
assert!(err.to_string().contains("Invalid state transition"));
}
}