use nutype::nutype;
use serde::{Deserialize, Serialize};
use std::fmt;
use thiserror::Error;
use uuid::Uuid;
use crate::domain_types::{AgentId, AgentName};
#[nutype(derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
Display,
TryFrom,
Into
))]
pub struct AgentVersion(Uuid);
impl AgentVersion {
pub fn generate() -> Self {
Self::new(Uuid::new_v4())
}
pub fn parse(s: &str) -> Result<Self, String> {
let uuid = Uuid::parse_str(s).map_err(|e| format!("Invalid UUID format: {e}"))?;
Ok(Self::new(uuid))
}
}
#[nutype(
validate(greater_or_equal = 1),
derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
Display,
TryFrom,
Into
)
)]
pub struct VersionNumber(u64);
impl VersionNumber {
pub fn first() -> Self {
Self::try_new(1).expect("Version 1 should always be valid")
}
pub fn next(&self) -> Result<Self, VersionNumberError> {
Self::try_new(self.into_inner() + 1)
}
pub fn as_u64(&self) -> u64 {
self.into_inner()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub enum AgentLifecycleState {
Unloaded,
Loaded,
Ready,
Running,
Draining,
Stopped,
Failed,
}
impl AgentLifecycleState {
pub fn can_start(&self) -> bool {
matches!(self, Self::Ready)
}
pub fn can_drain(&self) -> bool {
matches!(self, Self::Running)
}
pub fn can_stop(&self) -> bool {
matches!(self, Self::Running | Self::Draining | Self::Ready)
}
pub fn is_terminal(&self) -> bool {
matches!(self, Self::Stopped | Self::Failed)
}
pub fn is_active(&self) -> bool {
matches!(self, Self::Running | Self::Draining)
}
pub fn valid_transitions(&self) -> Vec<Self> {
match self {
Self::Unloaded => vec![Self::Loaded, Self::Failed],
Self::Loaded => vec![Self::Ready, Self::Failed, Self::Unloaded],
Self::Ready => vec![Self::Running, Self::Stopped, Self::Failed],
Self::Running => vec![Self::Draining, Self::Stopped, Self::Failed],
Self::Draining => vec![Self::Stopped, Self::Failed],
Self::Stopped | Self::Failed => vec![], }
}
pub fn can_transition_to(&self, next: Self) -> bool {
self.valid_transitions().contains(&next)
}
}
impl fmt::Display for AgentLifecycleState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let state_str = match self {
Self::Unloaded => "unloaded",
Self::Loaded => "loaded",
Self::Ready => "ready",
Self::Running => "running",
Self::Draining => "draining",
Self::Stopped => "stopped",
Self::Failed => "failed",
};
write!(f, "{state_str}")
}
}
#[nutype(
validate(greater_or_equal = 1000, less_or_equal = 300_000), // 1 second to 5 minutes
derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
Display,
Default,
TryFrom,
Into
),
default = 30_000 // 30 seconds
)]
pub struct TransitionTimeout(u64);
impl TransitionTimeout {
pub fn from_secs(secs: u64) -> Result<Self, TransitionTimeoutError> {
Self::try_new(secs * 1000)
}
pub fn as_millis(&self) -> u64 {
self.into_inner()
}
pub fn as_secs(&self) -> u64 {
self.into_inner() / 1000
}
}
#[nutype(
validate(greater_or_equal = 5_000, less_or_equal = 600_000), // 5 seconds to 10 minutes
derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
Display,
Default,
TryFrom,
Into
),
default = 60_000 // 60 seconds
)]
pub struct DrainTimeout(u64);
impl DrainTimeout {
pub fn from_secs(secs: u64) -> Result<Self, DrainTimeoutError> {
Self::try_new(secs * 1000)
}
pub fn as_millis(&self) -> u64 {
self.into_inner()
}
pub fn as_secs(&self) -> u64 {
self.into_inner() / 1000
}
}
#[nutype(
validate(greater_or_equal = 0, less_or_equal = 10_000),
derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
Display,
Default,
TryFrom,
Into
),
default = 0
)]
pub struct PendingRequestCount(u32);
impl PendingRequestCount {
pub fn zero() -> Self {
Self::default()
}
pub fn increment(&self) -> Result<Self, PendingRequestCountError> {
Self::try_new(self.into_inner() + 1)
}
#[must_use]
pub fn decrement(&self) -> Self {
let current = self.into_inner();
if current > 0 {
Self::try_new(current - 1).unwrap_or_default()
} else {
Self::zero()
}
}
pub fn as_u32(&self) -> u32 {
self.into_inner()
}
pub fn has_pending(&self) -> bool {
self.into_inner() > 0
}
}
#[nutype(
validate(len_char_min = 1, len_char_max = 1000),
derive(
Debug,
Clone,
PartialEq,
Eq,
Serialize,
Deserialize,
Display,
TryFrom,
Into
)
)]
pub struct FailureReason(String);
impl FailureReason {
pub fn from_error<E: std::error::Error>(error: &E) -> Result<Self, FailureReasonError> {
Self::try_new(error.to_string())
}
pub fn from_reason(reason: &str) -> Result<Self, FailureReasonError> {
Self::try_new(reason.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentLifecycle {
pub agent_id: AgentId,
pub agent_name: Option<AgentName>,
pub version: AgentVersion,
pub version_number: VersionNumber,
pub current_state: AgentLifecycleState,
pub previous_state: Option<AgentLifecycleState>,
pub transition_timeout: TransitionTimeout,
pub drain_timeout: DrainTimeout,
pub pending_requests: PendingRequestCount,
pub failure_reason: Option<FailureReason>,
}
impl AgentLifecycle {
pub fn new(
agent_id: AgentId,
agent_name: Option<AgentName>,
version: AgentVersion,
version_number: VersionNumber,
) -> Self {
Self {
agent_id,
agent_name,
version,
version_number,
current_state: AgentLifecycleState::Unloaded,
previous_state: None,
transition_timeout: TransitionTimeout::default(),
drain_timeout: DrainTimeout::default(),
pending_requests: PendingRequestCount::zero(),
failure_reason: None,
}
}
pub fn transition_to(
&mut self,
new_state: AgentLifecycleState,
failure_reason: Option<FailureReason>,
) -> Result<(), StateTransitionError> {
if !self.current_state.can_transition_to(new_state) {
return Err(StateTransitionError::InvalidTransition {
from: self.current_state,
to: new_state,
});
}
self.previous_state = Some(self.current_state);
self.current_state = new_state;
self.failure_reason = failure_reason;
Ok(())
}
pub fn start(&mut self) -> Result<(), StateTransitionError> {
self.transition_to(AgentLifecycleState::Running, None)
}
pub fn start_draining(&mut self) -> Result<(), StateTransitionError> {
self.transition_to(AgentLifecycleState::Draining, None)
}
pub fn stop(&mut self) -> Result<(), StateTransitionError> {
self.transition_to(AgentLifecycleState::Stopped, None)
}
pub fn fail(&mut self, reason: FailureReason) -> Result<(), StateTransitionError> {
self.transition_to(AgentLifecycleState::Failed, Some(reason))
}
pub fn add_pending_request(&mut self) -> Result<(), StateTransitionError> {
self.pending_requests = self.pending_requests.increment().map_err(|_| {
StateTransitionError::TooManyPendingRequests {
current: self.pending_requests.as_u32(),
}
})?;
Ok(())
}
pub fn complete_request(&mut self) {
self.pending_requests = self.pending_requests.decrement();
}
pub fn is_ready_to_stop(&self) -> bool {
self.current_state == AgentLifecycleState::Draining && !self.pending_requests.has_pending()
}
pub fn get_timeout_for_state(&self) -> u64 {
match self.current_state {
AgentLifecycleState::Draining => self.drain_timeout.as_millis(),
_ => self.transition_timeout.as_millis(),
}
}
}
#[derive(Debug, Clone, Error, PartialEq, Eq)]
pub enum StateTransitionError {
#[error("Invalid state transition from {from} to {to}")]
InvalidTransition {
from: AgentLifecycleState,
to: AgentLifecycleState,
},
#[error("Too many pending requests: {current}")]
TooManyPendingRequests { current: u32 },
#[error("Agent is in terminal state: {state}")]
TerminalState { state: AgentLifecycleState },
#[error("Transition timeout exceeded: {timeout_ms}ms")]
TimeoutExceeded { timeout_ms: u64 },
#[error("Agent already in target state: {state}")]
AlreadyInState { state: AgentLifecycleState },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LifecycleOperationResult {
pub success: bool,
pub previous_state: Option<AgentLifecycleState>,
pub current_state: AgentLifecycleState,
pub error: Option<String>,
}
impl LifecycleOperationResult {
pub fn success(
previous_state: Option<AgentLifecycleState>,
current_state: AgentLifecycleState,
) -> Self {
Self {
success: true,
previous_state,
current_state,
error: None,
}
}
pub fn failure(
previous_state: Option<AgentLifecycleState>,
current_state: AgentLifecycleState,
error: String,
) -> Self {
Self {
success: false,
previous_state,
current_state,
error: Some(error),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_agent_lifecycle_state_transitions() {
let unloaded = AgentLifecycleState::Unloaded;
assert!(unloaded.can_transition_to(AgentLifecycleState::Loaded));
assert!(unloaded.can_transition_to(AgentLifecycleState::Failed));
assert!(!unloaded.can_transition_to(AgentLifecycleState::Running));
let loaded = AgentLifecycleState::Loaded;
assert!(loaded.can_transition_to(AgentLifecycleState::Ready));
assert!(loaded.can_transition_to(AgentLifecycleState::Failed));
assert!(loaded.can_transition_to(AgentLifecycleState::Unloaded));
let running = AgentLifecycleState::Running;
assert!(running.can_transition_to(AgentLifecycleState::Draining));
assert!(running.can_transition_to(AgentLifecycleState::Stopped));
assert!(running.can_transition_to(AgentLifecycleState::Failed));
}
#[test]
fn test_pending_request_count() {
let mut count = PendingRequestCount::zero();
assert_eq!(count.as_u32(), 0);
assert!(!count.has_pending());
count = count.increment().unwrap();
assert_eq!(count.as_u32(), 1);
assert!(count.has_pending());
count = count.decrement();
assert_eq!(count.as_u32(), 0);
assert!(!count.has_pending());
}
#[test]
fn test_agent_lifecycle_new() {
let agent_id = AgentId::generate();
let agent_name = Some(AgentName::try_new("test-agent".to_string()).unwrap());
let version = AgentVersion::generate();
let version_number = VersionNumber::first();
let lifecycle = AgentLifecycle::new(agent_id, agent_name, version, version_number);
assert_eq!(lifecycle.current_state, AgentLifecycleState::Unloaded);
assert_eq!(lifecycle.previous_state, None);
assert!(!lifecycle.pending_requests.has_pending());
}
}