use nutype::nutype;
use serde::{Deserialize, Serialize};
use std::time::{Duration, SystemTime};
use thiserror::Error;
use uuid::Uuid;
use super::agent_lifecycle::{AgentVersion, VersionNumber};
use super::statistics::calculate_percentage_f32;
use crate::domain_types::{AgentId, AgentName, CpuFuel, MemoryBytes};
#[nutype(derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
Display,
TryFrom,
Into
))]
pub struct DeploymentId(Uuid);
impl DeploymentId {
pub fn generate() -> Self {
Self::new(Uuid::new_v4())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub enum DeploymentStrategy {
Immediate,
Rolling,
BlueGreen,
Canary,
}
impl DeploymentStrategy {
pub fn supports_gradual_rollout(&self) -> bool {
matches!(self, Self::Rolling | Self::Canary)
}
pub fn supports_instant_rollback(&self) -> bool {
matches!(self, Self::BlueGreen | Self::Canary)
}
}
#[nutype(
validate(greater_or_equal = 1, less_or_equal = 100),
derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
Display,
Default,
TryFrom,
Into
),
default = 1
)]
pub struct BatchSize(u8);
impl BatchSize {
pub fn as_u8(&self) -> u8 {
self.into_inner()
}
pub fn from_percentage(percentage: u8, total_instances: usize) -> Result<Self, BatchSizeError> {
if percentage > 100 {
return Err(Self::try_new(101).unwrap_err()); }
let calculated_size = (total_instances * usize::from(percentage)).div_ceil(100);
let batch_size = u8::try_from(calculated_size).unwrap_or(u8::MAX).max(1);
Self::try_new(batch_size)
}
}
#[nutype(
validate(greater_or_equal = 30_000, less_or_equal = 1_800_000), // 30 seconds to 30 minutes
derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
Display,
Default,
TryFrom,
Into
),
default = 300_000 // 5 minutes
)]
pub struct DeploymentTimeout(u64);
impl DeploymentTimeout {
pub fn from_mins(mins: u64) -> Result<Self, DeploymentTimeoutError> {
Self::try_new(mins * 60 * 1000)
}
pub fn as_millis(&self) -> u64 {
self.into_inner()
}
pub fn as_duration(&self) -> Duration {
Duration::from_millis(self.into_inner())
}
}
#[nutype(
validate(greater_or_equal = 1_048_576, less_or_equal = 1_073_741_824), // 1MB to 1GB
derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
Display,
Default,
TryFrom,
Into
),
default = 10_485_760 // 10MB
)]
pub struct DeploymentMemoryLimit(usize);
impl DeploymentMemoryLimit {
pub fn from_mb(mb: usize) -> Result<Self, DeploymentMemoryLimitError> {
Self::try_new(mb * 1024 * 1024)
}
pub fn as_bytes(&self) -> usize {
self.into_inner()
}
pub fn as_memory_bytes(&self) -> MemoryBytes {
MemoryBytes::try_new(self.into_inner()).unwrap_or_default()
}
}
#[nutype(
validate(greater_or_equal = 10_000, less_or_equal = 100_000_000), // 10K to 100M
derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
Display,
Default,
TryFrom,
Into
),
default = 1_000_000 // 1M
)]
pub struct DeploymentFuelLimit(u64);
impl DeploymentFuelLimit {
pub fn as_u64(&self) -> u64 {
self.into_inner()
}
pub fn as_cpu_fuel(&self) -> CpuFuel {
CpuFuel::try_new(self.into_inner()).unwrap_or_default()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HealthCheckConfig {
pub enabled: bool,
pub initial_delay: Duration,
pub interval: Duration,
pub timeout: Duration,
pub success_threshold: u32,
pub failure_threshold: u32,
}
impl Default for HealthCheckConfig {
fn default() -> Self {
Self {
enabled: true,
initial_delay: Duration::from_secs(10),
interval: Duration::from_secs(30),
timeout: Duration::from_secs(5),
success_threshold: 2,
failure_threshold: 3,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResourceRequirements {
pub memory_limit: DeploymentMemoryLimit,
pub fuel_limit: DeploymentFuelLimit,
pub requires_isolation: bool,
pub max_concurrent_requests: Option<u32>,
}
impl ResourceRequirements {
pub fn new(memory_limit: DeploymentMemoryLimit, fuel_limit: DeploymentFuelLimit) -> Self {
Self {
memory_limit,
fuel_limit,
requires_isolation: true,
max_concurrent_requests: Some(100),
}
}
pub fn minimal() -> Self {
Self {
memory_limit: DeploymentMemoryLimit::try_new(1_048_576).unwrap(), fuel_limit: DeploymentFuelLimit::try_new(10_000).unwrap(), requires_isolation: false,
max_concurrent_requests: Some(1),
}
}
pub fn is_compatible_with(&self, system_memory: usize, system_fuel: u64) -> bool {
self.memory_limit.as_bytes() <= system_memory && self.fuel_limit.as_u64() <= system_fuel
}
}
impl Default for ResourceRequirements {
fn default() -> Self {
Self::new(
DeploymentMemoryLimit::default(),
DeploymentFuelLimit::default(),
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeploymentConfig {
pub strategy: DeploymentStrategy,
pub batch_size: BatchSize,
pub timeout: DeploymentTimeout,
pub resource_requirements: ResourceRequirements,
pub health_check: HealthCheckConfig,
pub auto_rollback: bool,
pub rollback_threshold_percentage: u8,
}
impl DeploymentConfig {
pub fn new(strategy: DeploymentStrategy) -> Self {
Self {
strategy,
batch_size: BatchSize::default(),
timeout: DeploymentTimeout::default(),
resource_requirements: ResourceRequirements::default(),
health_check: HealthCheckConfig::default(),
auto_rollback: true,
rollback_threshold_percentage: 10,
}
}
pub fn immediate() -> Self {
Self::new(DeploymentStrategy::Immediate)
}
pub fn rolling(batch_size: BatchSize) -> Self {
let mut config = Self::new(DeploymentStrategy::Rolling);
config.batch_size = batch_size;
config
}
pub fn canary() -> Self {
let mut config = Self::new(DeploymentStrategy::Canary);
config.batch_size = BatchSize::try_new(1).unwrap(); config.health_check.enabled = true;
config
}
}
impl Default for DeploymentConfig {
fn default() -> Self {
Self::new(DeploymentStrategy::Rolling)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeploymentRequest {
pub deployment_id: DeploymentId,
pub agent_id: AgentId,
pub agent_name: Option<AgentName>,
pub from_version: Option<AgentVersion>,
pub to_version: AgentVersion,
pub to_version_number: VersionNumber,
pub config: DeploymentConfig,
pub wasm_module_bytes: Vec<u8>,
pub requested_at: SystemTime,
}
impl DeploymentRequest {
pub fn new(
agent_id: AgentId,
agent_name: Option<AgentName>,
from_version: Option<AgentVersion>,
to_version: AgentVersion,
to_version_number: VersionNumber,
config: DeploymentConfig,
wasm_module_bytes: Vec<u8>,
) -> Self {
Self {
deployment_id: DeploymentId::generate(),
agent_id,
agent_name,
from_version,
to_version,
to_version_number,
config,
wasm_module_bytes,
requested_at: SystemTime::now(),
}
}
pub fn is_initial_deployment(&self) -> bool {
self.from_version.is_none()
}
pub fn is_upgrade(&self) -> bool {
self.from_version.is_some()
}
pub fn module_size(&self) -> usize {
self.wasm_module_bytes.len()
}
pub fn validate(&self) -> Result<(), DeploymentValidationError> {
if self.wasm_module_bytes.is_empty() {
return Err(DeploymentValidationError::EmptyWasmModule);
}
if self.wasm_module_bytes.len() > 50 * 1024 * 1024 {
return Err(DeploymentValidationError::WasmModuleTooLarge {
size: self.wasm_module_bytes.len(),
max: 50 * 1024 * 1024,
});
}
if self.config.rollback_threshold_percentage > 100 {
return Err(DeploymentValidationError::InvalidRollbackThreshold {
threshold: self.config.rollback_threshold_percentage,
});
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub enum DeploymentStatus {
Pending,
InProgress,
Completed,
Failed,
Cancelled,
RolledBack,
}
impl DeploymentStatus {
pub fn is_terminal(&self) -> bool {
matches!(
self,
Self::Completed | Self::Failed | Self::Cancelled | Self::RolledBack
)
}
pub fn is_success(&self) -> bool {
matches!(self, Self::Completed)
}
pub fn is_failure(&self) -> bool {
matches!(self, Self::Failed | Self::Cancelled | Self::RolledBack)
}
}
#[nutype(
validate(greater_or_equal = 0, less_or_equal = 100),
derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
Display,
Default,
TryFrom,
Into
),
default = 0
)]
pub struct DeploymentProgress(u8);
impl DeploymentProgress {
pub fn from_percentage(percentage: u8) -> Result<Self, DeploymentProgressError> {
Self::try_new(percentage)
}
pub fn completed() -> Self {
Self::try_new(100).unwrap()
}
pub fn as_percentage(&self) -> u8 {
self.into_inner()
}
pub fn is_complete(&self) -> bool {
self.into_inner() == 100
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DeploymentResult {
pub deployment_id: DeploymentId,
pub agent_id: AgentId,
pub status: DeploymentStatus,
pub progress: DeploymentProgress,
pub started_at: Option<SystemTime>,
pub completed_at: Option<SystemTime>,
pub error_message: Option<String>,
pub rollback_version: Option<AgentVersion>,
pub metrics: Option<DeploymentMetrics>,
}
impl DeploymentResult {
pub fn success(
deployment_id: DeploymentId,
agent_id: AgentId,
started_at: SystemTime,
completed_at: SystemTime,
metrics: Option<DeploymentMetrics>,
) -> Self {
Self {
deployment_id,
agent_id,
status: DeploymentStatus::Completed,
progress: DeploymentProgress::completed(),
started_at: Some(started_at),
completed_at: Some(completed_at),
error_message: None,
rollback_version: None,
metrics,
}
}
pub fn failure(
deployment_id: DeploymentId,
agent_id: AgentId,
started_at: Option<SystemTime>,
error_message: String,
rollback_version: Option<AgentVersion>,
) -> Self {
Self {
deployment_id,
agent_id,
status: DeploymentStatus::Failed,
progress: DeploymentProgress::default(),
started_at,
completed_at: Some(SystemTime::now()),
error_message: Some(error_message),
rollback_version,
metrics: None,
}
}
pub fn duration(&self) -> Option<Duration> {
match (self.started_at, self.completed_at) {
(Some(start), Some(end)) => end.duration_since(start).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DeploymentMetrics {
pub instances_deployed: u32,
pub instances_failed: u32,
pub total_duration: Duration,
pub average_instance_deployment_time: Duration,
pub memory_usage_peak: usize,
pub fuel_consumed: u64,
pub health_check_success_rate: f32,
}
impl DeploymentMetrics {
pub fn success_rate_percentage(&self) -> f32 {
let total = self.instances_deployed + self.instances_failed;
if total == 0 {
return 0.0;
}
calculate_percentage_f32(u64::from(self.instances_deployed), u64::from(total))
}
pub fn meets_success_threshold(&self, threshold_percentage: u8) -> bool {
self.success_rate_percentage() >= f32::from(threshold_percentage)
}
}
#[derive(Debug, Clone, Error, PartialEq, Eq)]
pub enum DeploymentValidationError {
#[error("WASM module is empty")]
EmptyWasmModule,
#[error("WASM module too large: {size} bytes, max {max} bytes")]
WasmModuleTooLarge { size: usize, max: usize },
#[error("Invalid rollback threshold: {threshold}%, must be 0-100")]
InvalidRollbackThreshold { threshold: u8 },
#[error("Resource requirements exceed system limits")]
ResourceLimitsExceeded,
#[error("Invalid deployment strategy for current state")]
InvalidStrategy,
#[error("Missing required deployment configuration: {field}")]
MissingConfiguration { field: String },
}
#[derive(Debug, Clone, Error, PartialEq, Eq)]
pub enum DeploymentError {
#[error("Deployment validation failed: {0}")]
ValidationFailed(#[from] DeploymentValidationError),
#[error("Deployment timeout exceeded: {timeout}ms")]
TimeoutExceeded { timeout: u64 },
#[error("Insufficient resources: {resource}")]
InsufficientResources { resource: String },
#[error("Agent not found: {agent_id}")]
AgentNotFound { agent_id: AgentId },
#[error("Deployment already in progress: {deployment_id}")]
AlreadyInProgress { deployment_id: DeploymentId },
#[error("Health check failed: {reason}")]
HealthCheckFailed { reason: String },
#[error("Rollback failed: {reason}")]
RollbackFailed { reason: String },
#[error("WASM module validation failed: {reason}")]
WasmValidationFailed { reason: String },
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deployment_strategy() {
let rolling = DeploymentStrategy::Rolling;
assert!(rolling.supports_gradual_rollout());
assert!(!rolling.supports_instant_rollback());
let blue_green = DeploymentStrategy::BlueGreen;
assert!(!blue_green.supports_gradual_rollout());
assert!(blue_green.supports_instant_rollback());
}
#[test]
fn test_batch_size_from_percentage() {
let batch_size = BatchSize::from_percentage(10, 100).unwrap();
assert_eq!(batch_size.as_u8(), 10);
let batch_size = BatchSize::from_percentage(50, 3).unwrap();
assert_eq!(batch_size.as_u8(), 2);
let batch_size = BatchSize::from_percentage(1, 200).unwrap();
assert_eq!(batch_size.as_u8(), 2);
}
#[test]
fn test_deployment_request_validation() {
let agent_id = AgentId::generate();
let version = AgentVersion::generate();
let version_number = VersionNumber::first();
let config = DeploymentConfig::default();
let request = DeploymentRequest::new(
agent_id,
None,
None,
version,
version_number,
config.clone(),
vec![1, 2, 3, 4], );
assert!(request.validate().is_ok());
let empty_request = DeploymentRequest::new(
agent_id,
None,
None,
version,
version_number,
config.clone(),
vec![], );
assert!(matches!(
empty_request.validate(),
Err(DeploymentValidationError::EmptyWasmModule)
));
}
#[test]
fn test_deployment_progress() {
let progress = DeploymentProgress::from_percentage(50).unwrap();
assert_eq!(progress.as_percentage(), 50);
assert!(!progress.is_complete());
let completed = DeploymentProgress::completed();
assert_eq!(completed.as_percentage(), 100);
assert!(completed.is_complete());
}
#[test]
fn test_resource_requirements_compatibility() {
let requirements = ResourceRequirements::minimal();
assert!(requirements.is_compatible_with(10_000_000, 100_000));
assert!(!requirements.is_compatible_with(100, 100_000));
assert!(!requirements.is_compatible_with(10_000_000, 100));
}
}