use crate::core::error::{Error, Result};
use crate::lock_safe;
use crate::ml::serving::serialization::SerializableModel;
use crate::ml::serving::{
BatchPredictionRequest, BatchPredictionResponse, DeploymentConfig, HealthStatus, ModelInfo,
ModelMetadata, ModelServing, PredictionRequest, PredictionResponse,
};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
const MAX_TRACKED_REQUESTS: usize = 5_000;
const MAX_RECENT_ERRORS: usize = 200;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecordedError {
pub error_type: String,
pub message: String,
pub occurred_at: chrono::DateTime<chrono::Utc>,
}
fn error_type_name(error: &Error) -> &'static str {
match error {
Error::KeyNotFound(_) => "KeyNotFound",
Error::InvalidInput(_) => "InvalidInput",
Error::InvalidOperation(_) => "InvalidOperation",
Error::DimensionMismatch(_) => "DimensionMismatch",
Error::NotImplemented(_) => "NotImplemented",
Error::SerializationError(_) | Error::Json(_) | Error::JsonError(_) => "Serialization",
Error::Computation(_) => "Computation",
_ => "Other",
}
}
fn checked_cutoff(now: Instant, duration: Duration) -> Option<Instant> {
now.checked_sub(duration)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DeploymentStatus {
Starting,
Running,
Degraded,
Stopping,
Stopped,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeploymentMetrics {
pub status: DeploymentStatus,
pub active_instances: usize,
pub cpu_utilization: Option<f64>,
pub memory_utilization: Option<f64>,
pub request_rate: f64,
pub avg_response_time_ms: f64,
pub error_rate: f64,
pub total_requests: u64,
pub successful_requests: u64,
pub failed_requests: u64,
pub in_flight_requests: usize,
pub response_times_ms: Vec<u64>,
pub recent_errors: Vec<RecordedError>,
pub last_health_check: chrono::DateTime<chrono::Utc>,
pub started_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
pub struct DeployedModel {
model: Box<dyn ModelServing>,
config: DeploymentConfig,
metrics: Arc<Mutex<DeploymentMetrics>>,
stats: Arc<Mutex<RequestStats>>,
health_status: Arc<Mutex<HealthStatus>>,
}
#[derive(Debug, Clone)]
struct RequestStats {
entries: VecDeque<(Instant, Option<u64>)>,
recent_errors: VecDeque<RecordedError>,
error_count: u64,
success_count: u64,
in_flight: usize,
}
impl RequestStats {
fn new() -> Self {
Self {
entries: VecDeque::new(),
recent_errors: VecDeque::new(),
error_count: 0,
success_count: 0,
in_flight: 0,
}
}
fn record_success(&mut self, response_time_ms: u64) {
self.entries
.push_back((Instant::now(), Some(response_time_ms)));
self.success_count += 1;
self.evict_expired();
}
fn record_error(&mut self, error_type: String, message: String) {
self.entries.push_back((Instant::now(), None));
self.error_count += 1;
self.recent_errors.push_back(RecordedError {
error_type,
message,
occurred_at: chrono::Utc::now(),
});
while self.recent_errors.len() > MAX_RECENT_ERRORS {
self.recent_errors.pop_front();
}
self.evict_expired();
}
fn evict_expired(&mut self) {
let cutoff = checked_cutoff(Instant::now(), Duration::from_secs(300));
while let Some(&(t, _)) = self.entries.front() {
let expired = match cutoff {
Some(c) => t <= c,
None => false,
};
if expired {
self.entries.pop_front();
} else {
break;
}
}
while self.entries.len() > MAX_TRACKED_REQUESTS {
self.entries.pop_front();
}
}
fn calculate_request_rate(&self) -> f64 {
let cutoff = checked_cutoff(Instant::now(), Duration::from_secs(60));
let recent_requests = self
.entries
.iter()
.filter(|&&(time, _)| match cutoff {
Some(c) => time > c,
None => true,
})
.count();
recent_requests as f64 / 60.0
}
fn calculate_avg_response_time(&self) -> f64 {
let (sum, count) = self
.entries
.iter()
.filter_map(|&(_, outcome)| outcome)
.fold((0u64, 0u64), |(sum, count), latency| {
(sum + latency, count + 1)
});
if count == 0 {
0.0
} else {
sum as f64 / count as f64
}
}
fn calculate_error_rate(&self) -> f64 {
let total = self.success_count + self.error_count;
if total == 0 {
0.0
} else {
self.error_count as f64 / total as f64
}
}
fn response_times_snapshot(&self) -> Vec<u64> {
self.entries
.iter()
.filter_map(|&(_, outcome)| outcome)
.collect()
}
fn recent_errors_snapshot(&self) -> Vec<RecordedError> {
self.recent_errors.iter().cloned().collect()
}
}
impl DeployedModel {
pub fn new(model: Box<dyn ModelServing>, config: DeploymentConfig) -> Result<Self> {
let metrics = Arc::new(Mutex::new(DeploymentMetrics {
status: DeploymentStatus::Starting,
active_instances: 1,
cpu_utilization: None,
memory_utilization: None,
request_rate: 0.0,
avg_response_time_ms: 0.0,
error_rate: 0.0,
total_requests: 0,
successful_requests: 0,
failed_requests: 0,
in_flight_requests: 0,
response_times_ms: Vec::new(),
recent_errors: Vec::new(),
last_health_check: chrono::Utc::now(),
started_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
}));
let stats = Arc::new(Mutex::new(RequestStats::new()));
let health_status = Arc::new(Mutex::new(HealthStatus {
status: "starting".to_string(),
details: HashMap::new(),
timestamp: chrono::Utc::now(),
}));
let deployed_model = Self {
model,
config,
metrics,
stats,
health_status,
};
deployed_model.update_health_status()?;
{
let probe_healthy = {
let health = lock_safe!(
deployed_model.health_status,
"deployment health status lock"
)?;
health.status == "healthy"
};
let mut metrics = lock_safe!(deployed_model.metrics, "deployment metrics lock")?;
metrics.status = if probe_healthy {
DeploymentStatus::Running
} else {
DeploymentStatus::Failed
};
metrics.updated_at = chrono::Utc::now();
}
Ok(deployed_model)
}
pub fn get_config(&self) -> &DeploymentConfig {
&self.config
}
pub fn get_metrics(&self) -> Result<DeploymentMetrics> {
Ok(lock_safe!(self.metrics, "deployment metrics lock for get")?.clone())
}
fn update_metrics(&self) -> Result<()> {
let stats = lock_safe!(self.stats, "deployment stats lock")?;
let mut metrics = lock_safe!(self.metrics, "deployment metrics lock for update")?;
metrics.request_rate = stats.calculate_request_rate();
metrics.avg_response_time_ms = stats.calculate_avg_response_time();
metrics.error_rate = stats.calculate_error_rate();
metrics.total_requests = stats.success_count + stats.error_count;
metrics.successful_requests = stats.success_count;
metrics.failed_requests = stats.error_count;
metrics.in_flight_requests = stats.in_flight;
metrics.response_times_ms = stats.response_times_snapshot();
metrics.recent_errors = stats.recent_errors_snapshot();
metrics.updated_at = chrono::Utc::now();
metrics.cpu_utilization = None;
metrics.memory_utilization = None;
Ok(())
}
fn update_health_status(&self) -> Result<()> {
let health_result = self.model.health_check();
let mut health_status = lock_safe!(self.health_status, "deployment health status lock")?;
match health_result {
Ok(status) => {
*health_status = status;
}
Err(e) => {
health_status.status = "unhealthy".to_string();
health_status.details.clear();
health_status
.details
.insert("error".to_string(), e.to_string());
health_status.timestamp = chrono::Utc::now();
}
}
{
let mut metrics = lock_safe!(self.metrics, "deployment metrics lock")?;
metrics.last_health_check = chrono::Utc::now();
if health_status.status == "healthy" {
if metrics.status == DeploymentStatus::Degraded {
metrics.status = DeploymentStatus::Running;
}
} else if health_status.status == "unhealthy" {
metrics.status = DeploymentStatus::Degraded;
}
}
Ok(())
}
fn queue_pressure(&self, metrics: &DeploymentMetrics) -> f64 {
let ceiling = self.config.resources.max_concurrent_requests;
if ceiling == 0 {
0.0
} else {
metrics.in_flight_requests as f64 / ceiling as f64
}
}
fn latency_pressure(&self, metrics: &DeploymentMetrics) -> f64 {
let timeout_ms = self.config.health_check.timeout_seconds as f64 * 1000.0;
if timeout_ms <= 0.0 {
0.0
} else {
metrics.avg_response_time_ms / timeout_ms
}
}
pub fn should_scale_up(&self) -> Result<bool> {
let metrics = lock_safe!(self.metrics, "deployment metrics lock")?;
let config = &self.config.scaling;
Ok(self.latency_pressure(&metrics) > config.scale_up_threshold
|| self.queue_pressure(&metrics) > config.scale_up_threshold)
}
pub fn should_scale_down(&self) -> Result<bool> {
let metrics = lock_safe!(self.metrics, "deployment metrics lock")?;
let config = &self.config.scaling;
Ok(metrics.active_instances > config.min_instances
&& self.latency_pressure(&metrics) < config.scale_down_threshold
&& self.queue_pressure(&metrics) < config.scale_down_threshold)
}
pub fn scale_up(&self) -> Result<()> {
let mut metrics = lock_safe!(self.metrics, "deployment metrics lock")?;
let config = &self.config.scaling;
if metrics.active_instances < config.max_instances {
metrics.active_instances += 1;
metrics.updated_at = chrono::Utc::now();
log::info!(
"Scaled up deployment to {} instances",
metrics.active_instances
);
}
Ok(())
}
pub fn scale_down(&self) -> Result<()> {
let mut metrics = lock_safe!(self.metrics, "deployment metrics lock")?;
let config = &self.config.scaling;
if metrics.active_instances > config.min_instances {
metrics.active_instances -= 1;
metrics.updated_at = chrono::Utc::now();
log::info!(
"Scaled down deployment to {} instances",
metrics.active_instances
);
}
Ok(())
}
pub fn stop(&self) -> Result<()> {
let mut metrics = lock_safe!(self.metrics, "deployment metrics lock")?;
metrics.status = DeploymentStatus::Stopping;
metrics.updated_at = chrono::Utc::now();
metrics.status = DeploymentStatus::Stopped;
metrics.active_instances = 0;
Ok(())
}
pub fn restart(&self) -> Result<()> {
self.stop()?;
let mut metrics = lock_safe!(self.metrics, "deployment metrics lock")?;
metrics.status = DeploymentStatus::Starting;
metrics.active_instances = self.config.scaling.min_instances;
metrics.updated_at = chrono::Utc::now();
drop(metrics);
self.update_health_status()?;
let probe_healthy = {
let health = lock_safe!(self.health_status, "deployment health status lock")?;
health.status == "healthy"
};
let mut metrics = lock_safe!(self.metrics, "deployment metrics lock")?;
metrics.status = if probe_healthy {
DeploymentStatus::Running
} else {
DeploymentStatus::Failed
};
Ok(())
}
}
impl ModelServing for DeployedModel {
fn predict(&self, request: &PredictionRequest) -> Result<PredictionResponse> {
let start_time = Instant::now();
{
let metrics = lock_safe!(self.metrics, "deployment metrics lock")?;
if metrics.status != DeploymentStatus::Running {
return Err(Error::InvalidOperation(format!(
"Deployment is not running (status: {:?})",
metrics.status
)));
}
}
{
let mut stats = lock_safe!(self.stats, "deployment stats lock (in-flight enter)")?;
stats.in_flight += 1;
}
let result = self.model.predict(request);
let processing_time = start_time.elapsed().as_millis() as u64;
{
let mut stats = lock_safe!(self.stats, "deployment stats lock (record)")?;
stats.in_flight = stats.in_flight.saturating_sub(1);
match &result {
Ok(_) => stats.record_success(processing_time),
Err(e) => stats.record_error(error_type_name(e).to_string(), e.to_string()),
}
}
self.update_metrics()?;
result
}
fn predict_batch(&self, request: &BatchPredictionRequest) -> Result<BatchPredictionResponse> {
let start_time = Instant::now();
{
let metrics = lock_safe!(self.metrics, "deployment metrics lock")?;
if metrics.status != DeploymentStatus::Running {
return Err(Error::InvalidOperation(format!(
"Deployment is not running (status: {:?})",
metrics.status
)));
}
}
{
let mut stats = lock_safe!(self.stats, "deployment stats lock (in-flight enter)")?;
stats.in_flight += 1;
}
let result = self.model.predict_batch(request);
let processing_time = start_time.elapsed().as_millis() as u64;
{
let mut stats = lock_safe!(self.stats, "deployment stats lock (record)")?;
stats.in_flight = stats.in_flight.saturating_sub(1);
match &result {
Ok(response) => {
let denom = request.data.len().max(1) as f64;
let avg_time_ms = (processing_time as f64 / denom).round().max(0.0) as u64;
for _ in 0..response.summary.successful_predictions {
stats.record_success(avg_time_ms);
}
if response.summary.failed_items.is_empty() {
for _ in 0..response.summary.failed_predictions {
stats.record_error(
"BatchItemFailed".to_string(),
"batch item failed (no per-item detail reported by the model)"
.to_string(),
);
}
} else {
for (idx, message) in &response.summary.failed_items {
stats.record_error(
"BatchItemFailed".to_string(),
format!("item {}: {}", idx, message),
);
}
}
}
Err(e) => stats.record_error(error_type_name(e).to_string(), e.to_string()),
}
}
self.update_metrics()?;
result
}
fn get_metadata(&self) -> &ModelMetadata {
self.model.get_metadata()
}
fn health_check(&self) -> Result<HealthStatus> {
self.update_health_status()?;
Ok(lock_safe!(self.health_status, "deployment health status lock")?.clone())
}
fn info(&self) -> ModelInfo {
let mut info = self.model.info();
info.configuration.insert(
"deployment_config".to_string(),
serde_json::to_value(&self.config).unwrap_or(serde_json::Value::Null),
);
info.configuration.insert(
"deployment_metrics".to_string(),
self.get_metrics()
.ok()
.and_then(|m| serde_json::to_value(&m).ok())
.unwrap_or(serde_json::Value::Null),
);
info
}
fn to_serializable(&self) -> Result<SerializableModel> {
self.model.to_serializable()
}
}
pub struct DeploymentManager {
deployments: HashMap<String, DeployedModel>,
configs: HashMap<String, DeploymentConfig>,
}
impl DeploymentManager {
pub fn new() -> Self {
Self {
deployments: HashMap::new(),
configs: HashMap::new(),
}
}
pub fn deploy(
&mut self,
deployment_name: String,
model: Box<dyn ModelServing>,
config: DeploymentConfig,
) -> Result<()> {
if self.deployments.contains_key(&deployment_name) {
return Err(Error::InvalidOperation(format!(
"Deployment '{}' already exists",
deployment_name
)));
}
let deployed_model = DeployedModel::new(model, config.clone())?;
self.deployments
.insert(deployment_name.clone(), deployed_model);
self.configs.insert(deployment_name, config);
Ok(())
}
pub fn undeploy(&mut self, deployment_name: &str) -> Result<()> {
if let Some(deployment) = self.deployments.get(deployment_name) {
deployment.stop()?;
}
self.deployments.remove(deployment_name);
self.configs.remove(deployment_name);
Ok(())
}
pub fn get_deployment(&self, deployment_name: &str) -> Option<&DeployedModel> {
self.deployments.get(deployment_name)
}
pub fn list_deployments(&self) -> Vec<String> {
self.deployments.keys().cloned().collect()
}
pub fn get_deployment_metrics(&self, deployment_name: &str) -> Option<DeploymentMetrics> {
self.deployments
.get(deployment_name)
.and_then(|deployment| deployment.get_metrics().ok())
}
pub fn scale_deployment(&self, deployment_name: &str, target_instances: usize) -> Result<()> {
let deployment = self.deployments.get(deployment_name).ok_or_else(|| {
Error::KeyNotFound(format!("Deployment '{}' not found", deployment_name))
})?;
let current_instances = deployment.get_metrics()?.active_instances;
if target_instances > current_instances {
for _ in current_instances..target_instances {
deployment.scale_up()?;
}
} else if target_instances < current_instances {
for _ in target_instances..current_instances {
deployment.scale_down()?;
}
}
Ok(())
}
pub fn auto_scale_all(&self) -> Result<()> {
for deployment in self.deployments.values() {
if deployment.should_scale_up()? {
deployment.scale_up()?;
} else if deployment.should_scale_down()? {
deployment.scale_down()?;
}
}
Ok(())
}
pub fn health_check_all(&self) -> HashMap<String, HealthStatus> {
let mut results = HashMap::new();
for (name, deployment) in &self.deployments {
match deployment.health_check() {
Ok(status) => {
results.insert(name.clone(), status);
}
Err(e) => {
results.insert(
name.clone(),
HealthStatus {
status: "error".to_string(),
details: {
let mut details = HashMap::new();
details.insert("error".to_string(), e.to_string());
details
},
timestamp: chrono::Utc::now(),
},
);
}
}
}
results
}
}
impl Default for DeploymentManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ml::serving::{HealthCheckConfig, MonitoringConfig, ResourceConfig, ScalingConfig};
fn create_test_config() -> DeploymentConfig {
DeploymentConfig {
model_name: "test_model".to_string(),
model_version: "1.0.0".to_string(),
environment: "test".to_string(),
resources: ResourceConfig {
cpu_cores: 1.0,
memory_mb: 1024,
gpu_memory_mb: None,
max_concurrent_requests: 10,
},
scaling: ScalingConfig {
min_instances: 1,
max_instances: 5,
target_cpu_utilization: 0.7,
target_memory_utilization: 0.8,
scale_up_threshold: 0.8,
scale_down_threshold: 0.3,
},
health_check: HealthCheckConfig {
path: "/health".to_string(),
interval_seconds: 30,
timeout_seconds: 5,
failure_threshold: 3,
success_threshold: 2,
},
monitoring: MonitoringConfig {
enable_metrics: true,
enable_logging: true,
enable_tracing: false,
metrics_interval_seconds: 60,
log_level: "info".to_string(),
},
}
}
#[test]
fn test_deployment_status() {
let status = DeploymentStatus::Running;
assert_eq!(status, DeploymentStatus::Running);
}
#[test]
fn test_request_stats() {
let mut stats = RequestStats::new();
stats.record_success(100);
stats.record_success(150);
stats.record_error(
"InvalidInput".to_string(),
"missing feature 'x'".to_string(),
);
assert!(stats.calculate_avg_response_time() > 0.0);
assert!(stats.calculate_error_rate() > 0.0);
assert_eq!(stats.success_count, 2);
assert_eq!(stats.error_count, 1);
assert!((stats.calculate_avg_response_time() - 125.0).abs() < 1e-9);
let errors = stats.recent_errors_snapshot();
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].error_type, "InvalidInput");
assert_eq!(errors[0].message, "missing feature 'x'");
}
#[test]
fn test_deployment_manager() {
let manager = DeploymentManager::new();
assert!(manager.list_deployments().is_empty());
let config = create_test_config();
assert_eq!(config.model_name, "test_model");
assert_eq!(config.scaling.min_instances, 1);
assert_eq!(config.scaling.max_instances, 5);
}
}