saorsa_core/placement/
traits.rs

1// Copyright (c) 2025 Saorsa Labs Limited
2//
3// This file is part of the Saorsa P2P network.
4//
5// Licensed under the AGPL-3.0 license:
6// <https://www.gnu.org/licenses/agpl-3.0.html>
7
8//! Trait definitions for the placement system
9//!
10//! Defines pluggable interfaces for placement strategies, network topology,
11//! performance estimation, and constraint validation.
12
13use std::collections::{HashMap, HashSet};
14use std::time::Duration;
15
16use async_trait::async_trait;
17use serde::{Deserialize, Serialize};
18
19use crate::adaptive::{NodeId, performance::PerformanceMonitor, trust::EigenTrustEngine};
20use crate::placement::{GeographicLocation, NetworkRegion, PlacementDecision, PlacementResult};
21
22/// Core trait for placement strategies
23#[async_trait]
24pub trait PlacementStrategy: Send + Sync + std::fmt::Debug {
25    /// Select optimal nodes for placement
26    async fn select_nodes(
27        &mut self,
28        candidates: &HashSet<NodeId>,
29        replication_factor: u8,
30        trust_system: &EigenTrustEngine,
31        performance_monitor: &PerformanceMonitor,
32        node_metadata: &HashMap<NodeId, (GeographicLocation, u32, NetworkRegion)>,
33    ) -> PlacementResult<PlacementDecision>;
34
35    /// Get strategy name
36    fn name(&self) -> &str;
37
38    /// Check if strategy supports constraints
39    fn supports_constraints(&self) -> bool;
40}
41
42/// Network topology interface for geographic and structural information
43#[async_trait]
44pub trait NetworkTopology: Send + Sync {
45    /// Get geographic location of a node
46    async fn get_location(&self, node_id: &NodeId) -> Option<GeographicLocation>;
47
48    /// Get network region of a node
49    async fn get_region(&self, node_id: &NodeId) -> Option<NetworkRegion>;
50
51    /// Get ASN (Autonomous System Number) of a node
52    async fn get_asn(&self, node_id: &NodeId) -> Option<u32>;
53
54    /// Calculate network distance between two nodes
55    async fn network_distance(&self, node_a: &NodeId, node_b: &NodeId) -> Option<Duration>;
56
57    /// Check if two nodes are in the same network segment
58    async fn same_network_segment(&self, node_a: &NodeId, node_b: &NodeId) -> bool;
59}
60
61/// Performance estimation interface
62#[async_trait]
63pub trait PerformanceEstimator: Send + Sync {
64    /// Get node performance metrics
65    async fn get_metrics(&self, node_id: &NodeId) -> Option<NodePerformanceMetrics>;
66
67    /// Predict node availability for the next period
68    async fn predict_availability(&self, node_id: &NodeId, period: Duration) -> f64;
69
70    /// Get node capacity utilization
71    async fn get_capacity_utilization(&self, node_id: &NodeId) -> f64;
72
73    /// Estimate operation latency for a node
74    async fn estimate_latency(&self, node_id: &NodeId) -> Duration;
75}
76
77/// Constraint validation interface
78#[async_trait]
79pub trait PlacementConstraint: Send + Sync {
80    /// Check if a node satisfies this constraint
81    async fn validate_node(&self, node_id: &NodeId) -> bool;
82
83    /// Check if a set of nodes satisfies this constraint
84    async fn validate_selection(&self, nodes: &[NodeId]) -> bool;
85
86    /// Get constraint name for debugging
87    fn name(&self) -> &str;
88
89    /// Get constraint severity (higher = more important)
90    fn severity(&self) -> u8;
91}
92
93/// Final placement validation interface
94#[async_trait]
95pub trait PlacementValidator: Send + Sync {
96    /// Validate a complete placement decision
97    async fn validate(&self, decision: &PlacementDecision) -> PlacementResult<()>;
98
99    /// Get validator name
100    fn name(&self) -> &str;
101}
102
103/// Comprehensive node performance metrics
104#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
105pub struct NodePerformanceMetrics {
106    /// Node availability (0.0 - 1.0)
107    pub availability: f64,
108    /// Average latency in milliseconds
109    pub latency_ms: f64,
110    /// Bandwidth capacity in bytes per second
111    pub bandwidth_bps: u64,
112    /// Storage capacity in bytes
113    pub storage_capacity: u64,
114    /// Current storage utilization (0.0 - 1.0)
115    pub storage_utilization: f64,
116    /// CPU utilization (0.0 - 1.0)
117    pub cpu_utilization: f64,
118    /// Memory utilization (0.0 - 1.0)
119    pub memory_utilization: f64,
120    /// Network reliability score (0.0 - 1.0)
121    pub network_reliability: f64,
122    /// Churn rate (disconnections per hour)
123    pub churn_rate: f64,
124    /// Age of the node in hours
125    pub node_age_hours: f64,
126}
127
128impl NodePerformanceMetrics {
129    /// Create new metrics with validation
130    pub fn new(
131        availability: f64,
132        latency_ms: f64,
133        bandwidth_bps: u64,
134        storage_capacity: u64,
135        storage_utilization: f64,
136        cpu_utilization: f64,
137        memory_utilization: f64,
138        network_reliability: f64,
139        churn_rate: f64,
140        node_age_hours: f64,
141    ) -> PlacementResult<Self> {
142        // Validate percentage values
143        for (name, value) in [
144            ("availability", availability),
145            ("storage_utilization", storage_utilization),
146            ("cpu_utilization", cpu_utilization),
147            ("memory_utilization", memory_utilization),
148            ("network_reliability", network_reliability),
149        ] {
150            if !(0.0..=1.0).contains(&value) {
151                return Err(crate::placement::PlacementError::InvalidMetrics {
152                    field: name.to_string(),
153                    value,
154                    reason: "Must be between 0.0 and 1.0".to_string(),
155                });
156            }
157        }
158
159        // Validate non-negative values
160        if latency_ms < 0.0 {
161            return Err(crate::placement::PlacementError::InvalidMetrics {
162                field: "latency_ms".to_string(),
163                value: latency_ms,
164                reason: "Must be non-negative".to_string(),
165            });
166        }
167
168        if churn_rate < 0.0 {
169            return Err(crate::placement::PlacementError::InvalidMetrics {
170                field: "churn_rate".to_string(),
171                value: churn_rate,
172                reason: "Must be non-negative".to_string(),
173            });
174        }
175
176        if node_age_hours < 0.0 {
177            return Err(crate::placement::PlacementError::InvalidMetrics {
178                field: "node_age_hours".to_string(),
179                value: node_age_hours,
180                reason: "Must be non-negative".to_string(),
181            });
182        }
183
184        Ok(Self {
185            availability,
186            latency_ms,
187            bandwidth_bps,
188            storage_capacity,
189            storage_utilization,
190            cpu_utilization,
191            memory_utilization,
192            network_reliability,
193            churn_rate,
194            node_age_hours,
195        })
196    }
197
198    /// Calculate overall performance score (0.0 - 1.0)
199    pub fn overall_score(&self) -> f64 {
200        let weights = [
201            (self.availability, 0.3),
202            (1.0 - self.latency_ms / 1000.0, 0.2), // Invert latency
203            (1.0 - self.storage_utilization, 0.1),
204            (1.0 - self.cpu_utilization, 0.1),
205            (1.0 - self.memory_utilization, 0.1),
206            (self.network_reliability, 0.1),
207            ((1.0 / (1.0 + self.churn_rate)), 0.1), // Invert churn rate
208        ];
209
210        let weighted_sum: f64 = weights.iter().map(|(score, weight)| score * weight).sum();
211        weighted_sum.clamp(0.0, 1.0)
212    }
213
214    /// Check if node is suitable for storage
215    pub fn is_suitable_for_storage(&self) -> bool {
216        self.availability >= 0.8
217            && self.storage_utilization <= 0.9
218            && self.network_reliability >= 0.7
219            && self.churn_rate <= 2.0 // Max 2 disconnections per hour
220    }
221
222    /// Estimate remaining capacity
223    pub fn remaining_capacity(&self) -> u64 {
224        ((1.0 - self.storage_utilization) * self.storage_capacity as f64) as u64
225    }
226}
227
228impl Default for NodePerformanceMetrics {
229    fn default() -> Self {
230        Self {
231            availability: 0.9,
232            latency_ms: 50.0,
233            bandwidth_bps: 1_000_000,         // 1 Mbps
234            storage_capacity: 10_000_000_000, // 10 GB
235            storage_utilization: 0.5,
236            cpu_utilization: 0.3,
237            memory_utilization: 0.4,
238            network_reliability: 0.8,
239            churn_rate: 0.5,
240            node_age_hours: 168.0, // 1 week
241        }
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn test_node_performance_metrics_validation() {
251        // Test valid metrics
252        let metrics = NodePerformanceMetrics::new(
253            0.9,
254            50.0,
255            1_000_000,
256            10_000_000_000,
257            0.5,
258            0.3,
259            0.4,
260            0.8,
261            0.5,
262            168.0,
263        );
264        assert!(metrics.is_ok());
265
266        // Test invalid availability
267        let metrics = NodePerformanceMetrics::new(
268            1.5,
269            50.0,
270            1_000_000,
271            10_000_000_000,
272            0.5,
273            0.3,
274            0.4,
275            0.8,
276            0.5,
277            168.0,
278        );
279        assert!(metrics.is_err());
280
281        // Test negative latency
282        let metrics = NodePerformanceMetrics::new(
283            0.9,
284            -10.0,
285            1_000_000,
286            10_000_000_000,
287            0.5,
288            0.3,
289            0.4,
290            0.8,
291            0.5,
292            168.0,
293        );
294        assert!(metrics.is_err());
295    }
296
297    #[test]
298    fn test_overall_score_calculation() {
299        let metrics = NodePerformanceMetrics::new(
300            1.0,
301            10.0,
302            1_000_000,
303            10_000_000_000,
304            0.2,
305            0.1,
306            0.1,
307            0.9,
308            0.1,
309            168.0,
310        )
311        .unwrap();
312
313        let score = metrics.overall_score();
314        assert!(score >= 0.0 && score <= 1.0);
315        assert!(score > 0.8); // Should be high for good metrics
316    }
317
318    #[test]
319    fn test_storage_suitability() {
320        // Good node
321        let good_metrics = NodePerformanceMetrics::new(
322            0.95,
323            20.0,
324            1_000_000,
325            10_000_000_000,
326            0.3,
327            0.2,
328            0.2,
329            0.9,
330            0.1,
331            168.0,
332        )
333        .unwrap();
334        assert!(good_metrics.is_suitable_for_storage());
335
336        // Poor availability
337        let poor_availability = NodePerformanceMetrics::new(
338            0.5,
339            20.0,
340            1_000_000,
341            10_000_000_000,
342            0.3,
343            0.2,
344            0.2,
345            0.9,
346            0.1,
347            168.0,
348        )
349        .unwrap();
350        assert!(!poor_availability.is_suitable_for_storage());
351
352        // High storage utilization
353        let full_storage = NodePerformanceMetrics::new(
354            0.95,
355            20.0,
356            1_000_000,
357            10_000_000_000,
358            0.95,
359            0.2,
360            0.2,
361            0.9,
362            0.1,
363            168.0,
364        )
365        .unwrap();
366        assert!(!full_storage.is_suitable_for_storage());
367    }
368
369    #[test]
370    fn test_remaining_capacity_calculation() {
371        let metrics = NodePerformanceMetrics::new(
372            0.9,
373            50.0,
374            1_000_000,
375            1_000_000_000,
376            0.3,
377            0.3,
378            0.4,
379            0.8,
380            0.5,
381            168.0,
382        )
383        .unwrap();
384
385        let remaining = metrics.remaining_capacity();
386        assert_eq!(remaining, 700_000_000); // 70% of 1GB
387    }
388}