init projectmycelium
This commit is contained in:
406
src/services/slice_calculator.rs
Normal file
406
src/services/slice_calculator.rs
Normal file
@@ -0,0 +1,406 @@
|
||||
//! Slice calculator service for automatic slice calculation from node capacity
|
||||
//! Follows the established builder pattern for consistent API design
|
||||
|
||||
use crate::models::user::{NodeCapacity, FarmNode};
|
||||
use rust_decimal::Decimal;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
/// Base slice unit definition (1 vCPU, 4GB RAM, 200GB storage)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SliceUnit {
|
||||
pub cpu_cores: u32, // 1
|
||||
pub memory_gb: u32, // 4
|
||||
pub storage_gb: u32, // 200
|
||||
}
|
||||
|
||||
impl Default for SliceUnit {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cpu_cores: 1,
|
||||
memory_gb: 4,
|
||||
storage_gb: 200,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculated slice combination from node capacity
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SliceCombination {
|
||||
pub id: String,
|
||||
pub multiplier: u32, // How many base slices this uses
|
||||
pub cpu_cores: u32, // Slice-specific resource
|
||||
pub memory_gb: u32, // Slice-specific resource
|
||||
pub storage_gb: u32, // Slice-specific resource
|
||||
pub quantity_available: u32, // How many of this combination available
|
||||
pub price_per_hour: Decimal,
|
||||
pub base_slices_required: u32,
|
||||
|
||||
// Inherited from parent node
|
||||
pub node_uptime_percentage: f64,
|
||||
pub node_bandwidth_mbps: u32,
|
||||
pub node_location: String,
|
||||
pub node_certification_type: String,
|
||||
pub node_id: String,
|
||||
pub farmer_email: String,
|
||||
}
|
||||
|
||||
/// Track individual slice rentals
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SliceAllocation {
|
||||
pub allocation_id: String,
|
||||
pub slice_combination_id: String,
|
||||
pub renter_email: String,
|
||||
pub base_slices_used: u32,
|
||||
pub rental_start: DateTime<Utc>,
|
||||
pub rental_end: Option<DateTime<Utc>>,
|
||||
pub status: AllocationStatus,
|
||||
pub monthly_cost: Decimal,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum AllocationStatus {
|
||||
Active,
|
||||
Expired,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// Pricing configuration for node slices
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SlicePricing {
|
||||
pub base_price_per_hour: Decimal, // Price for 1 base slice per hour
|
||||
pub currency: String,
|
||||
pub pricing_multiplier: Decimal, // Farmer can adjust pricing (0.5x - 2.0x)
|
||||
}
|
||||
|
||||
impl Default for SlicePricing {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base_price_per_hour: Decimal::from(1), // $1 per hour for base slice
|
||||
currency: "USD".to_string(),
|
||||
pricing_multiplier: Decimal::from(1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Service for slice calculations following builder pattern
|
||||
#[derive(Clone)]
|
||||
pub struct SliceCalculatorService {
|
||||
base_slice: SliceUnit,
|
||||
pricing_limits: PricingLimits,
|
||||
}
|
||||
|
||||
/// Platform-enforced pricing limits
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PricingLimits {
|
||||
pub min_price_per_hour: Decimal, // e.g., $0.10
|
||||
pub max_price_per_hour: Decimal, // e.g., $10.00
|
||||
}
|
||||
|
||||
impl Default for PricingLimits {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
min_price_per_hour: Decimal::from_str_exact("0.10").unwrap(),
|
||||
max_price_per_hour: Decimal::from_str_exact("10.00").unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for SliceCalculatorService
|
||||
#[derive(Default)]
|
||||
pub struct SliceCalculatorServiceBuilder {
|
||||
base_slice: Option<SliceUnit>,
|
||||
pricing_limits: Option<PricingLimits>,
|
||||
}
|
||||
|
||||
impl SliceCalculatorServiceBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn base_slice(mut self, base_slice: SliceUnit) -> Self {
|
||||
self.base_slice = Some(base_slice);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn pricing_limits(mut self, limits: PricingLimits) -> Self {
|
||||
self.pricing_limits = Some(limits);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<SliceCalculatorService, String> {
|
||||
Ok(SliceCalculatorService {
|
||||
base_slice: self.base_slice.unwrap_or_default(),
|
||||
pricing_limits: self.pricing_limits.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SliceCalculatorService {
|
||||
pub fn builder() -> SliceCalculatorServiceBuilder {
|
||||
SliceCalculatorServiceBuilder::new()
|
||||
}
|
||||
|
||||
/// Calculate maximum base slices from node capacity
|
||||
pub fn calculate_max_base_slices(&self, capacity: &NodeCapacity) -> u32 {
|
||||
let cpu_slices = capacity.cpu_cores as u32 / self.base_slice.cpu_cores;
|
||||
let memory_slices = capacity.memory_gb as u32 / self.base_slice.memory_gb;
|
||||
let storage_slices = capacity.storage_gb as u32 / self.base_slice.storage_gb;
|
||||
|
||||
// Return the limiting factor
|
||||
std::cmp::min(std::cmp::min(cpu_slices, memory_slices), storage_slices)
|
||||
}
|
||||
|
||||
/// Generate all possible slice combinations from available base slices
|
||||
pub fn generate_slice_combinations(
|
||||
&self,
|
||||
max_base_slices: u32,
|
||||
allocated_slices: u32,
|
||||
node: &FarmNode,
|
||||
farmer_email: &str
|
||||
) -> Vec<SliceCombination> {
|
||||
let available_base_slices = max_base_slices.saturating_sub(allocated_slices);
|
||||
let mut combinations = Vec::new();
|
||||
|
||||
if available_base_slices == 0 {
|
||||
return combinations;
|
||||
}
|
||||
|
||||
// Generate practical slice combinations up to a reasonable limit
|
||||
// We'll generate combinations for multipliers 1x, 2x, 3x, 4x, 5x, 6x, 8x, 10x, 12x, 16x, 20x, 24x, 32x
|
||||
let practical_multipliers = vec![1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32];
|
||||
|
||||
for multiplier in practical_multipliers {
|
||||
// Skip if multiplier is larger than available slices
|
||||
if multiplier > available_base_slices {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate how many complete units of this multiplier we can create
|
||||
let quantity = available_base_slices / multiplier;
|
||||
|
||||
// Skip if we can't create at least one complete unit
|
||||
if quantity == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let combination = SliceCombination {
|
||||
id: format!("{}x{}", quantity, multiplier),
|
||||
multiplier,
|
||||
cpu_cores: self.base_slice.cpu_cores * multiplier,
|
||||
memory_gb: self.base_slice.memory_gb * multiplier,
|
||||
storage_gb: self.base_slice.storage_gb * multiplier,
|
||||
quantity_available: quantity,
|
||||
price_per_hour: self.calculate_combination_price(multiplier, node.slice_pricing.as_ref()
|
||||
.and_then(|sp| serde_json::from_value(sp.clone()).ok())
|
||||
.as_ref()
|
||||
.unwrap_or(&crate::services::slice_calculator::SlicePricing::default())),
|
||||
base_slices_required: multiplier,
|
||||
|
||||
// Inherited from parent node
|
||||
node_uptime_percentage: node.uptime_percentage as f64,
|
||||
node_bandwidth_mbps: node.capacity.bandwidth_mbps as u32,
|
||||
node_location: node.location.clone(),
|
||||
node_certification_type: node.grid_data.as_ref()
|
||||
.map(|g| g.get("certification_type")
|
||||
.and_then(|cert| cert.as_str())
|
||||
.unwrap_or("DIY")
|
||||
.to_string())
|
||||
.unwrap_or_else(|| "DIY".to_string()),
|
||||
node_id: node.id.clone(),
|
||||
farmer_email: farmer_email.to_string(),
|
||||
};
|
||||
|
||||
combinations.push(combination);
|
||||
}
|
||||
|
||||
// Sort by multiplier (smallest slices first)
|
||||
combinations.sort_by_key(|c| c.multiplier);
|
||||
combinations
|
||||
}
|
||||
|
||||
/// Generate slice combinations with explicit SLA values (for user-defined SLAs)
|
||||
pub fn generate_slice_combinations_with_sla(
|
||||
&self,
|
||||
max_base_slices: u32,
|
||||
allocated_slices: u32,
|
||||
node: &FarmNode,
|
||||
farmer_email: &str,
|
||||
uptime_percentage: f64,
|
||||
bandwidth_mbps: u32,
|
||||
base_price_per_hour: Decimal
|
||||
) -> Vec<SliceCombination> {
|
||||
let available_base_slices = max_base_slices.saturating_sub(allocated_slices);
|
||||
let mut combinations = Vec::new();
|
||||
|
||||
if available_base_slices == 0 {
|
||||
return combinations;
|
||||
}
|
||||
|
||||
// Generate practical slice combinations up to a reasonable limit
|
||||
// We'll generate combinations for multipliers 1x, 2x, 3x, 4x, 5x, 6x, 8x, 10x, 12x, 16x, 20x, 24x, 32x
|
||||
let practical_multipliers = vec![1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32];
|
||||
|
||||
// Create custom pricing with user's base price
|
||||
let custom_pricing = SlicePricing {
|
||||
base_price_per_hour,
|
||||
currency: "USD".to_string(),
|
||||
pricing_multiplier: Decimal::from(1),
|
||||
};
|
||||
|
||||
for multiplier in practical_multipliers {
|
||||
// Skip if multiplier is larger than available slices
|
||||
if multiplier > available_base_slices {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate how many complete units of this multiplier we can create
|
||||
let quantity = available_base_slices / multiplier;
|
||||
|
||||
// Skip if we can't create at least one complete unit
|
||||
if quantity == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let combination = SliceCombination {
|
||||
id: format!("{}x{}", quantity, multiplier),
|
||||
multiplier,
|
||||
cpu_cores: self.base_slice.cpu_cores * multiplier,
|
||||
memory_gb: self.base_slice.memory_gb * multiplier,
|
||||
storage_gb: self.base_slice.storage_gb * multiplier,
|
||||
quantity_available: quantity,
|
||||
price_per_hour: self.calculate_combination_price(multiplier, &custom_pricing),
|
||||
base_slices_required: multiplier,
|
||||
|
||||
// Use explicit SLA values instead of inheriting from node
|
||||
node_uptime_percentage: uptime_percentage,
|
||||
node_bandwidth_mbps: bandwidth_mbps,
|
||||
node_location: node.location.clone(),
|
||||
node_certification_type: node.grid_data.as_ref()
|
||||
.map(|g| g.get("certification_type")
|
||||
.and_then(|cert| cert.as_str())
|
||||
.unwrap_or("DIY")
|
||||
.to_string())
|
||||
.unwrap_or_else(|| "DIY".to_string()),
|
||||
node_id: node.id.clone(),
|
||||
farmer_email: farmer_email.to_string(),
|
||||
};
|
||||
|
||||
combinations.push(combination);
|
||||
}
|
||||
|
||||
// Sort by multiplier (smallest slices first)
|
||||
combinations.sort_by_key(|c| c.multiplier);
|
||||
combinations
|
||||
}
|
||||
|
||||
/// Calculate price for a slice combination
|
||||
fn calculate_combination_price(&self, multiplier: u32, pricing: &SlicePricing) -> Decimal {
|
||||
pricing.base_price_per_hour * pricing.pricing_multiplier * Decimal::from(multiplier)
|
||||
}
|
||||
|
||||
/// Update availability after rental
|
||||
pub fn update_availability_after_rental(
|
||||
&self,
|
||||
node: &mut FarmNode,
|
||||
rented_base_slices: u32,
|
||||
farmer_email: &str
|
||||
) -> Result<(), String> {
|
||||
// Update allocated count
|
||||
node.allocated_base_slices += rented_base_slices as i32;
|
||||
|
||||
// Recalculate available combinations
|
||||
let combinations = self.generate_slice_combinations(
|
||||
node.total_base_slices as u32,
|
||||
node.allocated_base_slices as u32,
|
||||
node,
|
||||
farmer_email
|
||||
);
|
||||
node.available_combinations = combinations.iter()
|
||||
.map(|c| serde_json::to_value(c).unwrap_or_default())
|
||||
.collect();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update availability after rental expiry
|
||||
pub fn update_availability_after_release(
|
||||
&self,
|
||||
node: &mut FarmNode,
|
||||
released_base_slices: u32,
|
||||
farmer_email: &str
|
||||
) -> Result<(), String> {
|
||||
// Update allocated count
|
||||
node.allocated_base_slices = node.allocated_base_slices.saturating_sub(released_base_slices as i32);
|
||||
|
||||
// Recalculate available combinations
|
||||
node.available_combinations = self.generate_slice_combinations(
|
||||
node.total_base_slices as u32,
|
||||
node.allocated_base_slices as u32,
|
||||
node,
|
||||
farmer_email
|
||||
).iter()
|
||||
.map(|c| serde_json::to_value(c).unwrap_or_default())
|
||||
.collect();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate slice price within platform limits
|
||||
pub fn validate_slice_price(&self, price: Decimal) -> Result<(), String> {
|
||||
if price < self.pricing_limits.min_price_per_hour {
|
||||
return Err(format!("Price too low. Minimum: ${}/hour", self.pricing_limits.min_price_per_hour));
|
||||
}
|
||||
if price > self.pricing_limits.max_price_per_hour {
|
||||
return Err(format!("Price too high. Maximum: ${}/hour", self.pricing_limits.max_price_per_hour));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Slice rental record for users with deployment options
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SliceRental {
|
||||
pub rental_id: String,
|
||||
pub slice_combination_id: String,
|
||||
pub node_id: String,
|
||||
pub farmer_email: String,
|
||||
pub slice_allocation: SliceAllocation,
|
||||
pub total_cost: Decimal,
|
||||
pub payment_status: PaymentStatus,
|
||||
#[serde(default)]
|
||||
pub slice_format: String,
|
||||
#[serde(default)]
|
||||
pub user_email: String,
|
||||
#[serde(default)]
|
||||
pub status: String,
|
||||
#[serde(default)]
|
||||
pub start_date: Option<chrono::DateTime<chrono::Utc>>,
|
||||
#[serde(default)]
|
||||
pub rental_duration_days: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub monthly_cost: Option<Decimal>,
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
// NEW: Deployment information
|
||||
#[serde(default)]
|
||||
pub deployment_type: Option<String>, // "vm" or "kubernetes"
|
||||
#[serde(default)]
|
||||
pub deployment_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub deployment_config: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub deployment_status: Option<String>, // "Provisioning", "Active", "Stopped", "Failed"
|
||||
#[serde(default)]
|
||||
pub deployment_endpoint: Option<String>, // Access URL/IP for the deployment
|
||||
#[serde(default)]
|
||||
pub deployment_metadata: Option<std::collections::HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum PaymentStatus {
|
||||
Pending,
|
||||
Paid,
|
||||
Failed,
|
||||
Refunded,
|
||||
}
|
Reference in New Issue
Block a user