use std::sync::Mutex;
use serde::{Deserialize, Serialize};
use crate::cost::CostOptimizationSuggestion;
use crate::error::StorageError;
use crate::storage::StorageProvider;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum ForecastAlgorithm {
#[default]
LinearRegression,
ExponentialSmoothing,
HoltWinters,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapacityPoint {
pub timestamp_day: u64,
pub capacity_bytes: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapacityForecast {
pub algorithm: ForecastAlgorithm,
pub horizon_days: u32,
pub forecast_points: Vec<CapacityPoint>,
pub confidence: f64,
pub lower_bound: Vec<CapacityPoint>,
pub upper_bound: Vec<CapacityPoint>,
pub mape: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderCostEstimate {
pub provider_name: String,
pub monthly_cost: f64,
pub price_per_gb: f64,
pub estimated_capacity_gb: f64,
pub recommended: bool,
pub recommendation_reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CostComparisonReport {
pub requested_capacity_bytes: u64,
pub provider_estimates: Vec<ProviderCostEstimate>,
pub best_provider: String,
pub max_saving: f64,
pub generated_at: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OptimizationExecutionResult {
pub suggestion: CostOptimizationSuggestion,
pub success: bool,
pub auto_executed: bool,
pub detail: String,
}
pub struct MultiCloudCostComparator {
provider_pricing: Mutex<Vec<ProviderPricing>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ProviderPricing {
name: String,
price_per_gb_month: f64,
}
impl MultiCloudCostComparator {
pub fn new() -> Self {
Self {
provider_pricing: Mutex::new(vec![
ProviderPricing {
name: "aws_s3".to_string(),
price_per_gb_month: 0.023,
},
ProviderPricing {
name: "aliyun_oss".to_string(),
price_per_gb_month: 0.012,
},
ProviderPricing {
name: "tencent_cos".to_string(),
price_per_gb_month: 0.0099,
},
ProviderPricing {
name: "huawei_obs".to_string(),
price_per_gb_month: 0.0099,
},
]),
}
}
pub fn add_provider(&self, name: impl Into<String>, price_per_gb_month: f64) {
self.provider_pricing.lock().unwrap().push(ProviderPricing {
name: name.into(),
price_per_gb_month,
});
}
pub fn compare_providers(
&self,
capacity_bytes: u64,
_providers: &[StorageProvider],
) -> Result<CostComparisonReport, StorageError> {
if capacity_bytes == 0 {
return Err(StorageError::InvalidConfig(
"capacity must be > 0".to_string(),
));
}
let pricing = self.provider_pricing.lock().unwrap();
let capacity_gb = capacity_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
let mut estimates: Vec<ProviderCostEstimate> = pricing
.iter()
.map(|p| {
let monthly_cost = p.price_per_gb_month * capacity_gb;
ProviderCostEstimate {
provider_name: p.name.clone(),
monthly_cost,
price_per_gb: p.price_per_gb_month,
estimated_capacity_gb: capacity_gb,
recommended: false,
recommendation_reason: String::new(),
}
})
.collect();
estimates.sort_by(|a, b| {
a.monthly_cost
.partial_cmp(&b.monthly_cost)
.unwrap_or(std::cmp::Ordering::Equal)
});
if let Some(best) = estimates.first_mut() {
best.recommended = true;
best.recommendation_reason = "lowest monthly cost".to_string();
}
let best_provider = estimates
.first()
.map(|e| e.provider_name.clone())
.unwrap_or_default();
let max_cost = estimates.last().map(|e| e.monthly_cost).unwrap_or(0.0);
let min_cost = estimates.first().map(|e| e.monthly_cost).unwrap_or(0.0);
let max_saving = max_cost - min_cost;
Ok(CostComparisonReport {
requested_capacity_bytes: capacity_bytes,
provider_estimates: estimates,
best_provider,
max_saving,
generated_at: now_ms(),
})
}
}
impl Default for MultiCloudCostComparator {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for MultiCloudCostComparator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MultiCloudCostComparator")
.field(
"provider_count",
&self.provider_pricing.lock().unwrap().len(),
)
.finish()
}
}
pub struct CapacityForecaster;
impl CapacityForecaster {
pub fn new() -> Self {
Self
}
pub fn forecast(
&self,
history: &[CapacityPoint],
algorithm: ForecastAlgorithm,
horizon_days: u32,
confidence: f64,
) -> Result<CapacityForecast, StorageError> {
if history.len() < 7 {
return Err(StorageError::InvalidConfig(format!(
"need at least 7 history points, got {}",
history.len()
)));
}
let confidence = confidence.clamp(0.0, 1.0);
let last_day = history.last().unwrap().timestamp_day;
let (forecast_points, mape) = match algorithm {
ForecastAlgorithm::LinearRegression => {
linear_regression_forecast(history, horizon_days, last_day)
}
ForecastAlgorithm::ExponentialSmoothing => {
exponential_smoothing_forecast(history, horizon_days, last_day, 0.3)
}
ForecastAlgorithm::HoltWinters => {
holt_winters_forecast(history, horizon_days, last_day)
}
};
let z_score = match confidence {
c if c >= 0.99 => 2.576,
c if c >= 0.95 => 1.96,
c if c >= 0.90 => 1.645,
c if c >= 0.80 => 1.282,
_ => 1.0,
};
let std_dev = calculate_std_dev(history);
let margin = z_score * std_dev;
let lower_bound: Vec<CapacityPoint> = forecast_points
.iter()
.map(|p| CapacityPoint {
timestamp_day: p.timestamp_day,
capacity_bytes: (p.capacity_bytes as f64 - margin).max(0.0) as u64,
})
.collect();
let upper_bound: Vec<CapacityPoint> = forecast_points
.iter()
.map(|p| CapacityPoint {
timestamp_day: p.timestamp_day,
capacity_bytes: (p.capacity_bytes as f64 + margin) as u64,
})
.collect();
Ok(CapacityForecast {
algorithm,
horizon_days,
forecast_points,
confidence,
lower_bound,
upper_bound,
mape,
})
}
}
impl Default for CapacityForecaster {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for CapacityForecaster {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CapacityForecaster").finish()
}
}
pub struct AutoOptimizer {
whitelist: Mutex<Vec<CostOptimizationSuggestion>>,
history: Mutex<Vec<OptimizationExecutionResult>>,
}
impl AutoOptimizer {
pub fn new() -> Self {
Self {
whitelist: Mutex::new(Vec::new()),
history: Mutex::new(Vec::new()),
}
}
pub fn with_whitelist(&self, whitelist: Vec<CostOptimizationSuggestion>) {
*self.whitelist.lock().unwrap() = whitelist;
}
pub fn execute_suggestion(
&self,
suggestion: &CostOptimizationSuggestion,
) -> Result<OptimizationExecutionResult, StorageError> {
let whitelist = self.whitelist.lock().unwrap();
let auto_executed = whitelist.contains(suggestion);
if !auto_executed {
return Err(StorageError::PermissionDenied(format!(
"suggestion {suggestion:?} requires manual confirmation"
)));
}
let detail = match suggestion {
CostOptimizationSuggestion::TierDowngrade {
bucket,
from_tier,
to_tier,
..
} => format!("tier downgrade: {bucket} {from_tier} -> {to_tier}"),
CostOptimizationSuggestion::LifecycleOptimize { bucket, .. } => {
format!("lifecycle optimize: {bucket}")
}
CostOptimizationSuggestion::DeleteExpired { bucket, .. } => {
format!("delete expired: {bucket}")
}
CostOptimizationSuggestion::CompressCold { bucket, .. } => {
format!("compress cold: {bucket}")
}
};
let result = OptimizationExecutionResult {
suggestion: suggestion.clone(),
success: true,
auto_executed,
detail,
};
self.history.lock().unwrap().push(result.clone());
Ok(result)
}
pub fn history(&self) -> Vec<OptimizationExecutionResult> {
self.history.lock().unwrap().clone()
}
}
impl Default for AutoOptimizer {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for AutoOptimizer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AutoOptimizer")
.field("whitelist_count", &self.whitelist.lock().unwrap().len())
.field("history_count", &self.history.lock().unwrap().len())
.finish()
}
}
fn now_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
fn linear_regression_forecast(
history: &[CapacityPoint],
horizon_days: u32,
last_day: u64,
) -> (Vec<CapacityPoint>, f64) {
let n = history.len() as f64;
let sum_x: f64 = (0..history.len()).map(|i| i as f64).sum();
let sum_y: f64 = history.iter().map(|p| p.capacity_bytes as f64).sum();
let sum_xy: f64 = history
.iter()
.enumerate()
.map(|(i, p)| i as f64 * p.capacity_bytes as f64)
.sum();
let sum_x2: f64 = (0..history.len()).map(|i| (i as f64).powi(2)).sum();
let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x);
let intercept = (sum_y - slope * sum_x) / n;
let mape = calculate_mape(history, |i| slope * i as f64 + intercept);
let forecast: Vec<CapacityPoint> = (1..=horizon_days)
.map(|d| CapacityPoint {
timestamp_day: last_day + d as u64,
capacity_bytes: (slope * (history.len() as f64 + d as f64 - 1.0) + intercept).max(0.0)
as u64,
})
.collect();
(forecast, mape)
}
fn exponential_smoothing_forecast(
history: &[CapacityPoint],
horizon_days: u32,
last_day: u64,
alpha: f64,
) -> (Vec<CapacityPoint>, f64) {
let mut smoothed = history[0].capacity_bytes as f64;
for p in &history[1..] {
smoothed = alpha * p.capacity_bytes as f64 + (1.0 - alpha) * smoothed;
}
let mape = calculate_mape(history, |_| smoothed);
let forecast: Vec<CapacityPoint> = (1..=horizon_days)
.map(|d| CapacityPoint {
timestamp_day: last_day + d as u64,
capacity_bytes: smoothed.max(0.0) as u64,
})
.collect();
(forecast, mape)
}
fn holt_winters_forecast(
history: &[CapacityPoint],
horizon_days: u32,
last_day: u64,
) -> (Vec<CapacityPoint>, f64) {
let alpha = 0.3;
let beta = 0.1;
let gamma = 0.1;
let season_length = 7.min(history.len());
let mut level = history[0].capacity_bytes as f64;
let mut trend = if history.len() > 1 {
history[1].capacity_bytes as f64 - history[0].capacity_bytes as f64
} else {
0.0
};
let mut seasonals: Vec<f64> = history
.iter()
.take(season_length)
.map(|p| p.capacity_bytes as f64 / level)
.collect();
for (i, p) in history.iter().enumerate().skip(season_length) {
let s = seasonals[i % season_length];
let new_level = alpha * (p.capacity_bytes as f64 / s) + (1.0 - alpha) * (level + trend);
let new_trend = beta * (new_level - level) + (1.0 - beta) * trend;
seasonals[i % season_length] =
gamma * (p.capacity_bytes as f64 / new_level) + (1.0 - gamma) * s;
level = new_level;
trend = new_trend;
}
let mape = calculate_mape(history, |i| {
let s = seasonals[i % season_length];
level + trend * (i as f64 + 1.0) * s
});
let forecast: Vec<CapacityPoint> = (1..=horizon_days)
.map(|d| {
let s = seasonals[(history.len() + d as usize - 1) % season_length];
CapacityPoint {
timestamp_day: last_day + d as u64,
capacity_bytes: ((level + trend * d as f64) * s).max(0.0) as u64,
}
})
.collect();
(forecast, mape)
}
fn calculate_mape(history: &[CapacityPoint], predictor: impl Fn(usize) -> f64) -> f64 {
let errors: Vec<f64> = history
.iter()
.enumerate()
.filter(|(_, p)| p.capacity_bytes > 0)
.map(|(i, p)| {
let predicted = predictor(i);
((p.capacity_bytes as f64 - predicted).abs() / p.capacity_bytes as f64) * 100.0
})
.collect();
if errors.is_empty() {
0.0
} else {
errors.iter().sum::<f64>() / errors.len() as f64
}
}
fn calculate_std_dev(history: &[CapacityPoint]) -> f64 {
if history.len() < 2 {
return 0.0;
}
let mean = history.iter().map(|p| p.capacity_bytes as f64).sum::<f64>() / history.len() as f64;
let variance = history
.iter()
.map(|p| (p.capacity_bytes as f64 - mean).powi(2))
.sum::<f64>()
/ history.len() as f64;
variance.sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
fn make_history(days: usize) -> Vec<CapacityPoint> {
(0..days)
.map(|i| CapacityPoint {
timestamp_day: i as u64,
capacity_bytes: (1000 + i * 100) as u64,
})
.collect()
}
#[test]
fn test_forecast_algorithm_default() {
assert_eq!(
ForecastAlgorithm::default(),
ForecastAlgorithm::LinearRegression
);
}
#[test]
fn test_multi_cloud_comparator_new() {
let comparator = MultiCloudCostComparator::new();
let report = comparator
.compare_providers(1024 * 1024 * 1024, &[])
.unwrap();
assert!(!report.provider_estimates.is_empty());
assert!(!report.best_provider.is_empty());
}
#[test]
fn test_multi_cloud_comparator_recommends_cheapest() {
let comparator = MultiCloudCostComparator::new();
let report = comparator
.compare_providers(100 * 1024 * 1024 * 1024, &[])
.unwrap();
let best = report
.provider_estimates
.iter()
.find(|e| e.recommended)
.unwrap();
let min_cost = report
.provider_estimates
.iter()
.map(|e| e.monthly_cost)
.fold(f64::INFINITY, f64::min);
assert_eq!(best.monthly_cost, min_cost);
}
#[test]
fn test_multi_cloud_comparator_zero_capacity() {
let comparator = MultiCloudCostComparator::new();
let result = comparator.compare_providers(0, &[]);
assert!(result.is_err());
}
#[test]
fn test_multi_cloud_comparator_custom_provider() {
let comparator = MultiCloudCostComparator::new();
comparator.add_provider("custom_cloud", 0.001);
let report = comparator
.compare_providers(1024 * 1024 * 1024, &[])
.unwrap();
assert!(report
.provider_estimates
.iter()
.any(|e| e.provider_name == "custom_cloud"));
}
#[test]
fn test_multi_cloud_comparator_max_saving() {
let comparator = MultiCloudCostComparator::new();
let report = comparator
.compare_providers(100 * 1024 * 1024 * 1024, &[])
.unwrap();
assert!(report.max_saving >= 0.0);
}
#[test]
fn test_capacity_forecaster_linear() {
let forecaster = CapacityForecaster::new();
let history = make_history(14);
let result = forecaster
.forecast(&history, ForecastAlgorithm::LinearRegression, 7, 0.95)
.unwrap();
assert_eq!(result.algorithm, ForecastAlgorithm::LinearRegression);
assert_eq!(result.forecast_points.len(), 7);
assert!(result.confidence > 0.0);
}
#[test]
fn test_capacity_forecaster_exponential() {
let forecaster = CapacityForecaster::new();
let history = make_history(10);
let result = forecaster
.forecast(&history, ForecastAlgorithm::ExponentialSmoothing, 5, 0.90)
.unwrap();
assert_eq!(result.forecast_points.len(), 5);
}
#[test]
fn test_capacity_forecaster_holt_winters() {
let forecaster = CapacityForecaster::new();
let history = make_history(14);
let result = forecaster
.forecast(&history, ForecastAlgorithm::HoltWinters, 7, 0.80)
.unwrap();
assert_eq!(result.forecast_points.len(), 7);
}
#[test]
fn test_capacity_forecaster_insufficient_data() {
let forecaster = CapacityForecaster::new();
let history = make_history(5);
let result = forecaster.forecast(&history, ForecastAlgorithm::LinearRegression, 7, 0.95);
assert!(result.is_err());
}
#[test]
fn test_capacity_forecaster_confidence_bounds() {
let forecaster = CapacityForecaster::new();
let history = make_history(14);
let result = forecaster
.forecast(&history, ForecastAlgorithm::LinearRegression, 7, 0.95)
.unwrap();
assert_eq!(result.lower_bound.len(), 7);
assert_eq!(result.upper_bound.len(), 7);
for (lower, upper) in result.lower_bound.iter().zip(result.upper_bound.iter()) {
assert!(lower.capacity_bytes <= upper.capacity_bytes);
}
}
#[test]
fn test_auto_optimizer_no_whitelist() {
let optimizer = AutoOptimizer::new();
let suggestion = CostOptimizationSuggestion::TierDowngrade {
bucket: "test".to_string(),
from_tier: "Standard".to_string(),
to_tier: "InfrequentAccess".to_string(),
expected_saving_percent: 60.0,
};
let result = optimizer.execute_suggestion(&suggestion);
assert!(result.is_err());
}
#[test]
fn test_auto_optimizer_with_whitelist() {
let optimizer = AutoOptimizer::new();
let suggestion = CostOptimizationSuggestion::TierDowngrade {
bucket: "test".to_string(),
from_tier: "Standard".to_string(),
to_tier: "InfrequentAccess".to_string(),
expected_saving_percent: 60.0,
};
optimizer.with_whitelist(vec![suggestion.clone()]);
let result = optimizer.execute_suggestion(&suggestion).unwrap();
assert!(result.success);
assert!(result.auto_executed);
}
#[test]
fn test_auto_optimizer_history() {
let optimizer = AutoOptimizer::new();
let suggestion = CostOptimizationSuggestion::DeleteExpired {
bucket: "test".to_string(),
expired_count: 100,
};
optimizer.with_whitelist(vec![suggestion.clone()]);
optimizer.execute_suggestion(&suggestion).unwrap();
assert_eq!(optimizer.history().len(), 1);
}
#[test]
fn test_capacity_point_serialize() {
let point = CapacityPoint {
timestamp_day: 100,
capacity_bytes: 1024,
};
let json = serde_json::to_string(&point).unwrap();
let deserialized: CapacityPoint = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.timestamp_day, 100);
assert_eq!(deserialized.capacity_bytes, 1024);
}
}