Files
projectmycelium/src/services/slice_rental.rs
2025-09-01 21:37:01 -04:00

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,
}