391 lines
15 KiB
Rust
391 lines
15 KiB
Rust
//! Slice rental service for managing slice rentals and availability
|
|
//! Follows the established builder pattern for consistent API design
|
|
|
|
use crate::services::slice_calculator::{SliceCalculatorService, SliceAllocation, SliceRental, AllocationStatus, PaymentStatus};
|
|
use crate::services::user_persistence::{UserPersistence, UserPersistentData};
|
|
use rust_decimal::Decimal;
|
|
use rust_decimal::prelude::*;
|
|
use std::str::FromStr;
|
|
use chrono::Utc;
|
|
use serde::{Serialize, Deserialize};
|
|
use std::fs::OpenOptions;
|
|
|
|
/// Service for slice rental operations
|
|
#[derive(Clone)]
|
|
pub struct SliceRentalService {
|
|
slice_calculator: SliceCalculatorService,
|
|
enable_file_locking: bool,
|
|
}
|
|
|
|
/// Builder for SliceRentalService
|
|
#[derive(Default)]
|
|
pub struct SliceRentalServiceBuilder {
|
|
slice_calculator: Option<SliceCalculatorService>,
|
|
enable_file_locking: Option<bool>,
|
|
}
|
|
|
|
impl SliceRentalServiceBuilder {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
pub fn slice_calculator(mut self, slice_calculator: SliceCalculatorService) -> Self {
|
|
self.slice_calculator = Some(slice_calculator);
|
|
self
|
|
}
|
|
|
|
pub fn enable_file_locking(mut self, enabled: bool) -> Self {
|
|
self.enable_file_locking = Some(enabled);
|
|
self
|
|
}
|
|
|
|
pub fn build(self) -> Result<SliceRentalService, String> {
|
|
let slice_calculator = self.slice_calculator.unwrap_or_else(|| {
|
|
SliceCalculatorService::builder().build().expect("Failed to create default SliceCalculatorService")
|
|
});
|
|
|
|
Ok(SliceRentalService {
|
|
slice_calculator,
|
|
enable_file_locking: self.enable_file_locking.unwrap_or(true),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl SliceRentalService {
|
|
pub fn builder() -> SliceRentalServiceBuilder {
|
|
SliceRentalServiceBuilder::new()
|
|
}
|
|
|
|
/// Rent a slice combination from a farmer's node
|
|
pub fn rent_slice_combination(
|
|
&self,
|
|
renter_email: &str,
|
|
farmer_email: &str,
|
|
node_id: &str,
|
|
combination_id: &str,
|
|
quantity: u32,
|
|
rental_duration_hours: u32,
|
|
) -> Result<SliceRental, String> {
|
|
// Atomic operation with file locking to prevent conflicts
|
|
if self.enable_file_locking {
|
|
self.rent_with_file_lock(renter_email, farmer_email, node_id, combination_id, quantity, rental_duration_hours)
|
|
} else {
|
|
self.rent_without_lock(renter_email, farmer_email, node_id, combination_id, quantity, rental_duration_hours)
|
|
}
|
|
}
|
|
|
|
/// Rent a slice combination with deployment options (VM/Kubernetes)
|
|
pub fn rent_slice_combination_with_deployment(
|
|
&self,
|
|
renter_email: &str,
|
|
farmer_email: &str,
|
|
node_id: &str,
|
|
combination_id: &str,
|
|
quantity: u32,
|
|
rental_duration_hours: u32,
|
|
deployment_type: &str,
|
|
deployment_name: &str,
|
|
deployment_config: Option<serde_json::Value>,
|
|
) -> Result<SliceRental, String> {
|
|
// First rent the slice combination
|
|
let mut rental = self.rent_slice_combination(
|
|
renter_email, farmer_email, node_id, combination_id, quantity, rental_duration_hours
|
|
)?;
|
|
|
|
// Add deployment metadata to the rental
|
|
rental.deployment_type = Some(deployment_type.to_string());
|
|
rental.deployment_name = Some(deployment_name.to_string());
|
|
rental.deployment_config = deployment_config;
|
|
rental.deployment_status = Some("Provisioning".to_string());
|
|
|
|
// Save the enhanced rental to user's persistent data
|
|
self.save_rental_to_user_data(renter_email, &rental)?;
|
|
|
|
Ok(rental)
|
|
}
|
|
|
|
/// Get user's slice rentals
|
|
pub fn get_user_slice_rentals(&self, user_email: &str) -> Vec<SliceRental> {
|
|
if let Some(persistent_data) = UserPersistence::load_user_data(user_email) {
|
|
persistent_data.slice_rentals
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
/// Save rental to user's persistent data
|
|
fn save_rental_to_user_data(&self, user_email: &str, rental: &SliceRental) -> Result<(), String> {
|
|
let mut persistent_data = UserPersistence::load_user_data(user_email)
|
|
.unwrap_or_else(|| UserPersistentData::default());
|
|
|
|
// Add or update the rental
|
|
if let Some(existing_index) = persistent_data.slice_rentals.iter().position(|r| r.rental_id == rental.rental_id) {
|
|
persistent_data.slice_rentals[existing_index] = rental.clone();
|
|
} else {
|
|
persistent_data.slice_rentals.push(rental.clone());
|
|
}
|
|
|
|
UserPersistence::save_user_data(&persistent_data)
|
|
.map_err(|e| format!("Failed to save rental to user data: {}", e))
|
|
}
|
|
|
|
/// Rent slice with file locking for atomic operations
|
|
fn rent_with_file_lock(
|
|
&self,
|
|
renter_email: &str,
|
|
farmer_email: &str,
|
|
node_id: &str,
|
|
combination_id: &str,
|
|
quantity: u32,
|
|
rental_duration_hours: u32,
|
|
) -> Result<SliceRental, String> {
|
|
// Create lock file
|
|
let lock_file_path = format!("./user_data/.lock_{}_{}", farmer_email.replace("@", "_"), node_id);
|
|
let _lock_file = OpenOptions::new()
|
|
.create(true)
|
|
.write(true)
|
|
.open(&lock_file_path)
|
|
.map_err(|e| format!("Failed to create lock file: {}", e))?;
|
|
|
|
let result = self.rent_without_lock(renter_email, farmer_email, node_id, combination_id, quantity, rental_duration_hours);
|
|
|
|
// Clean up lock file
|
|
let _ = std::fs::remove_file(&lock_file_path);
|
|
|
|
result
|
|
}
|
|
|
|
/// Rent slice without file locking
|
|
fn rent_without_lock(
|
|
&self,
|
|
renter_email: &str,
|
|
farmer_email: &str,
|
|
node_id: &str,
|
|
combination_id: &str,
|
|
quantity: u32,
|
|
rental_duration_hours: u32,
|
|
) -> Result<SliceRental, String> {
|
|
// Load farmer data
|
|
let mut farmer_data = UserPersistence::load_user_data(farmer_email)
|
|
.ok_or_else(|| "Farmer not found".to_string())?;
|
|
|
|
// Find the node
|
|
let node_index = farmer_data.nodes.iter().position(|n| n.id == node_id)
|
|
.ok_or_else(|| "Node not found".to_string())?;
|
|
|
|
let node = &mut farmer_data.nodes[node_index];
|
|
|
|
// Find the slice combination
|
|
let combination = node.available_combinations.iter()
|
|
.find(|c| c.get("id").and_then(|v| v.as_str()) == Some(combination_id))
|
|
.ok_or_else(|| "Slice combination not found".to_string())?
|
|
.clone();
|
|
|
|
// Check availability
|
|
let available_qty = combination.get("quantity_available").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
|
|
if available_qty < quantity {
|
|
return Err(format!("Insufficient availability. Available: {}, Requested: {}",
|
|
available_qty, quantity));
|
|
}
|
|
|
|
// Calculate costs
|
|
let base_slices_required = combination.get("base_slices_required").and_then(|v| v.as_u64()).unwrap_or(1) as u32;
|
|
let total_base_slices_needed = base_slices_required * quantity;
|
|
let price_per_hour = combination.get("price_per_hour")
|
|
.and_then(|p| p.as_str())
|
|
.and_then(|p_str| rust_decimal::Decimal::from_str(p_str).ok())
|
|
.unwrap_or_else(|| Decimal::from_f64(1.0).unwrap_or_default());
|
|
let hourly_cost = price_per_hour * Decimal::from(quantity);
|
|
let total_cost = hourly_cost * Decimal::from(rental_duration_hours);
|
|
|
|
// Check renter's balance
|
|
let mut renter_data = UserPersistence::load_user_data(renter_email)
|
|
.unwrap_or_else(|| self.create_default_user_data(renter_email));
|
|
|
|
if renter_data.wallet_balance_usd < total_cost {
|
|
return Err(format!("Insufficient balance. Required: ${}, Available: ${}",
|
|
total_cost, renter_data.wallet_balance_usd));
|
|
}
|
|
|
|
// Create allocation
|
|
let allocation_id = format!("alloc_{}", &uuid::Uuid::new_v4().to_string()[..8]);
|
|
let rental_id = format!("rental_{}", &uuid::Uuid::new_v4().to_string()[..8]);
|
|
|
|
let allocation = SliceAllocation {
|
|
allocation_id: allocation_id.clone(),
|
|
slice_combination_id: combination_id.to_string(),
|
|
renter_email: renter_email.to_string(),
|
|
base_slices_used: total_base_slices_needed,
|
|
rental_start: Utc::now(),
|
|
rental_end: Some(Utc::now() + chrono::Duration::hours(rental_duration_hours as i64)),
|
|
status: AllocationStatus::Active,
|
|
monthly_cost: hourly_cost * Decimal::from(24 * 30), // Approximate monthly cost
|
|
};
|
|
|
|
// Update node availability
|
|
self.slice_calculator.update_availability_after_rental(
|
|
node,
|
|
total_base_slices_needed,
|
|
farmer_email
|
|
)?;
|
|
|
|
// Add allocation to node
|
|
node.slice_allocations.push(serde_json::to_value(allocation.clone()).unwrap_or_default());
|
|
|
|
// Create rental record
|
|
let slice_rental = SliceRental {
|
|
rental_id: rental_id.clone(),
|
|
slice_combination_id: combination_id.to_string(),
|
|
node_id: node_id.to_string(),
|
|
farmer_email: farmer_email.to_string(),
|
|
slice_allocation: allocation,
|
|
total_cost,
|
|
payment_status: PaymentStatus::Paid,
|
|
slice_format: "standard".to_string(),
|
|
user_email: renter_email.to_string(),
|
|
status: "Active".to_string(),
|
|
start_date: Some(chrono::Utc::now()),
|
|
rental_duration_days: Some(30),
|
|
monthly_cost: Some(total_cost),
|
|
id: rental_id.clone(),
|
|
deployment_type: None,
|
|
deployment_name: None,
|
|
deployment_config: None,
|
|
deployment_status: None,
|
|
deployment_endpoint: None,
|
|
deployment_metadata: None,
|
|
};
|
|
|
|
// Deduct payment from renter
|
|
renter_data.wallet_balance_usd -= total_cost;
|
|
renter_data.slice_rentals.push(slice_rental.clone());
|
|
|
|
// Add earnings to farmer
|
|
farmer_data.wallet_balance_usd += total_cost;
|
|
|
|
// Save both user data
|
|
UserPersistence::save_user_data(&farmer_data)
|
|
.map_err(|e| format!("Failed to save farmer data: {}", e))?;
|
|
UserPersistence::save_user_data(&renter_data)
|
|
.map_err(|e| format!("Failed to save renter data: {}", e))?;
|
|
|
|
Ok(slice_rental)
|
|
}
|
|
|
|
/// Release expired slice rentals
|
|
pub fn release_expired_rentals(&self, farmer_email: &str) -> Result<u32, String> {
|
|
let mut farmer_data = UserPersistence::load_user_data(farmer_email)
|
|
.ok_or_else(|| "Farmer not found".to_string())?;
|
|
|
|
let mut released_count = 0;
|
|
let now = Utc::now();
|
|
|
|
for node in &mut farmer_data.nodes {
|
|
let mut expired_allocations = Vec::new();
|
|
|
|
// Find expired allocations
|
|
for (index, allocation) in node.slice_allocations.iter().enumerate() {
|
|
if let Some(end_time) = allocation.get("rental_end")
|
|
.and_then(|r| r.as_str())
|
|
.and_then(|r_str| chrono::DateTime::parse_from_rfc3339(r_str).ok())
|
|
.map(|dt| dt.with_timezone(&chrono::Utc)) {
|
|
if now > end_time && allocation.get("status")
|
|
.and_then(|s| s.as_str())
|
|
.map(|s| s == "Active")
|
|
.unwrap_or(false) {
|
|
expired_allocations.push(index);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove expired allocations and update availability
|
|
for &index in expired_allocations.iter().rev() {
|
|
let base_slices_used = {
|
|
let allocation = &mut node.slice_allocations[index];
|
|
if let Some(allocation_obj) = allocation.as_object_mut() {
|
|
allocation_obj.insert("status".to_string(), serde_json::Value::String("Expired".to_string()));
|
|
}
|
|
allocation.get("base_slices_used")
|
|
.and_then(|b| b.as_u64())
|
|
.unwrap_or(0) as u32
|
|
};
|
|
|
|
// Update availability
|
|
self.slice_calculator.update_availability_after_release(
|
|
node,
|
|
base_slices_used,
|
|
farmer_email
|
|
)?;
|
|
|
|
released_count += 1;
|
|
}
|
|
}
|
|
|
|
if released_count > 0 {
|
|
UserPersistence::save_user_data(&farmer_data)
|
|
.map_err(|e| format!("Failed to save farmer data: {}", e))?;
|
|
}
|
|
|
|
Ok(released_count)
|
|
}
|
|
|
|
|
|
/// Get slice rental statistics for a farmer
|
|
pub fn get_farmer_slice_statistics(&self, farmer_email: &str) -> SliceRentalStatistics {
|
|
if let Some(farmer_data) = UserPersistence::load_user_data(farmer_email) {
|
|
let mut stats = SliceRentalStatistics::default();
|
|
|
|
for node in &farmer_data.nodes {
|
|
stats.total_nodes += 1;
|
|
stats.total_base_slices += node.total_base_slices as u32;
|
|
stats.allocated_base_slices += node.allocated_base_slices as u32;
|
|
stats.active_rentals += node.slice_allocations.iter()
|
|
.filter(|a| a.get("status")
|
|
.and_then(|s| s.as_str())
|
|
.map(|s| s == "Active")
|
|
.unwrap_or(false))
|
|
.count() as u32;
|
|
|
|
// Calculate earnings from slice rentals
|
|
for allocation in &node.slice_allocations {
|
|
if allocation.get("status")
|
|
.and_then(|s| s.as_str())
|
|
.map(|s| s == "Active")
|
|
.unwrap_or(false) {
|
|
let monthly_cost = allocation.get("monthly_cost")
|
|
.and_then(|c| c.as_str())
|
|
.and_then(|c_str| rust_decimal::Decimal::from_str(c_str).ok())
|
|
.unwrap_or_default();
|
|
stats.monthly_earnings += monthly_cost;
|
|
}
|
|
}
|
|
}
|
|
|
|
stats.utilization_percentage = if stats.total_base_slices > 0 {
|
|
(stats.allocated_base_slices as f64 / stats.total_base_slices as f64) * 100.0
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
stats
|
|
} else {
|
|
SliceRentalStatistics::default()
|
|
}
|
|
}
|
|
|
|
/// Create default user data using centralized builder
|
|
fn create_default_user_data(&self, user_email: &str) -> UserPersistentData {
|
|
crate::models::builders::SessionDataBuilder::new_user(user_email)
|
|
}
|
|
}
|
|
|
|
/// Statistics for farmer slice rentals
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct SliceRentalStatistics {
|
|
pub total_nodes: u32,
|
|
pub total_base_slices: u32,
|
|
pub allocated_base_slices: u32,
|
|
pub active_rentals: u32,
|
|
pub utilization_percentage: f64,
|
|
pub monthly_earnings: Decimal,
|
|
} |