use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use thiserror::Error;
use tokio::sync::{RwLock, Semaphore};
use tracing::{debug, error, info, warn};
use voirs_sdk::{AudioBuffer, VoirsError};
use crate::privacy::{PrivacyConfig, PrivacyPreservingEvaluator, PrivateEvaluationResult};
use crate::quality::QualityEvaluator;
use crate::traits::{QualityEvaluationConfig, QualityEvaluator as QualityEvaluatorTrait};
#[derive(Error, Debug)]
pub enum FederatedError {
#[error("Node communication error with '{node}': {message}")]
CommunicationError {
node: String,
message: String,
},
#[error("Aggregation error: {message}")]
AggregationError {
message: String,
},
#[error("Node registration error: {message}")]
RegistrationError {
message: String,
},
#[error("Consensus error: {message}")]
ConsensusError {
message: String,
},
#[error("Operation timed out after {duration:?}")]
TimeoutError {
duration: Duration,
},
#[error("Invalid configuration: {message}")]
ConfigError {
message: String,
},
#[error("VoiRS error: {0}")]
VoirsError(#[from] VoirsError),
#[error("Privacy error: {0}")]
PrivacyError(#[from] crate::privacy::PrivacyError),
#[error("Evaluation error: {0}")]
EvaluationError(#[from] crate::EvaluationError),
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum NodeStatus {
Online,
Busy,
Offline,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeInfo {
pub node_id: String,
pub endpoint: String,
pub status: NodeStatus,
pub capabilities: NodeCapabilities,
pub last_heartbeat: u64,
pub total_evaluations: u64,
pub current_load: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeCapabilities {
pub metrics: Vec<String>,
pub max_concurrent: usize,
pub gpu_enabled: bool,
pub available_memory_mb: usize,
pub speed_factor: f64,
}
impl Default for NodeCapabilities {
fn default() -> Self {
Self {
metrics: vec!["pesq".to_string(), "stoi".to_string(), "mcd".to_string()],
max_concurrent: 4,
gpu_enabled: false,
available_memory_mb: 4096,
speed_factor: 1.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeConfig {
pub node_id: String,
pub coordinator_endpoint: String,
pub enable_privacy: bool,
pub privacy_config: Option<PrivacyConfig>,
pub heartbeat_interval_seconds: u64,
pub max_concurrent_evaluations: usize,
}
impl Default for NodeConfig {
fn default() -> Self {
Self {
node_id: "default_node".to_string(),
coordinator_endpoint: "http://localhost:8080".to_string(),
enable_privacy: true,
privacy_config: Some(PrivacyConfig::new()),
heartbeat_interval_seconds: 30,
max_concurrent_evaluations: 4,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoordinatorConfig {
pub min_nodes: usize,
pub max_nodes: usize,
pub aggregation_strategy: AggregationStrategy,
pub enable_secure_aggregation: bool,
pub consensus_threshold: f64,
pub node_timeout_seconds: u64,
pub enable_fault_tolerance: bool,
}
impl Default for CoordinatorConfig {
fn default() -> Self {
Self {
min_nodes: 2,
max_nodes: 100,
aggregation_strategy: AggregationStrategy::WeightedAverage,
enable_secure_aggregation: true,
consensus_threshold: 0.67, node_timeout_seconds: 300, enable_fault_tolerance: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AggregationStrategy {
Average,
WeightedAverage,
Median,
TrimmedMean,
FederatedLearning,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvaluationTask {
pub task_id: String,
pub task_type: String,
pub parameters: HashMap<String, serde_json::Value>,
pub priority: u32,
pub created_at: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeEvaluationResult {
pub node_id: String,
pub task_id: String,
pub quality_score: f64,
pub private_result: Option<PrivateEvaluationResult>,
pub duration_ms: u64,
pub weight: f64,
pub completed_at: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FederatedEvaluationResult {
pub task_id: String,
pub aggregated_score: f64,
pub confidence_interval: (f64, f64),
pub participant_count: usize,
pub node_results: Vec<NodeEvaluationResult>,
pub aggregation_strategy: AggregationStrategy,
pub total_duration_ms: u64,
pub privacy_guarantees: Vec<String>,
}
pub struct FederatedNode {
config: NodeConfig,
evaluator: Arc<RwLock<QualityEvaluator>>,
privacy_evaluator: Option<Arc<RwLock<PrivacyPreservingEvaluator>>>,
info: Arc<RwLock<NodeInfo>>,
semaphore: Arc<Semaphore>,
}
impl FederatedNode {
pub async fn new(config: NodeConfig) -> Result<Self, FederatedError> {
let evaluator = QualityEvaluator::new().await?;
let privacy_evaluator = if config.enable_privacy {
let privacy_config = config.privacy_config.clone().unwrap_or_default();
let pe = PrivacyPreservingEvaluator::new(privacy_config).await?;
Some(Arc::new(RwLock::new(pe)))
} else {
None
};
let info = NodeInfo {
node_id: config.node_id.clone(),
endpoint: config.coordinator_endpoint.clone(),
status: NodeStatus::Online,
capabilities: NodeCapabilities::default(),
last_heartbeat: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("value should be present")
.as_secs(),
total_evaluations: 0,
current_load: 0.0,
};
let semaphore = Arc::new(Semaphore::new(config.max_concurrent_evaluations));
Ok(Self {
config,
evaluator: Arc::new(RwLock::new(evaluator)),
privacy_evaluator,
info: Arc::new(RwLock::new(info)),
semaphore,
})
}
pub async fn execute_task(
&self,
task: &EvaluationTask,
audio: &AudioBuffer,
reference: Option<&AudioBuffer>,
) -> Result<NodeEvaluationResult, FederatedError> {
let _permit =
self.semaphore
.acquire()
.await
.map_err(|e| FederatedError::CommunicationError {
node: self.config.node_id.clone(),
message: format!("Failed to acquire semaphore: {}", e),
})?;
let start = SystemTime::now();
{
let mut info = self.info.write().await;
info.status = NodeStatus::Busy;
}
let result = if self.config.enable_privacy {
if let Some(ref pe) = self.privacy_evaluator {
let pe_lock = pe.read().await;
let private_result = pe_lock.evaluate_with_privacy(audio, reference).await?;
let quality_score = private_result.score;
let duration_ms = SystemTime::now()
.duration_since(start)
.unwrap_or(Duration::ZERO)
.as_millis() as u64;
NodeEvaluationResult {
node_id: self.config.node_id.clone(),
task_id: task.task_id.clone(),
quality_score,
private_result: Some(private_result),
duration_ms,
weight: 1.0,
completed_at: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("value should be present")
.as_secs(),
}
} else {
return Err(FederatedError::ConfigError {
message: "Privacy enabled but evaluator not initialized".to_string(),
});
}
} else {
let evaluator = self.evaluator.read().await;
let eval_config = QualityEvaluationConfig::default();
let quality = evaluator
.evaluate_quality(audio, reference, Some(&eval_config))
.await?;
let duration_ms = SystemTime::now()
.duration_since(start)
.unwrap_or(Duration::ZERO)
.as_millis() as u64;
NodeEvaluationResult {
node_id: self.config.node_id.clone(),
task_id: task.task_id.clone(),
quality_score: quality.overall_score as f64,
private_result: None,
duration_ms,
weight: 1.0,
completed_at: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("value should be present")
.as_secs(),
}
};
{
let mut info = self.info.write().await;
info.status = NodeStatus::Online;
info.total_evaluations += 1;
info.last_heartbeat = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("value should be present")
.as_secs();
}
Ok(result)
}
pub async fn get_info(&self) -> NodeInfo {
let info = self.info.read().await;
info.clone()
}
pub async fn heartbeat(&self) {
let mut info = self.info.write().await;
info.last_heartbeat = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("value should be present")
.as_secs();
}
}
pub struct FederatedCoordinator {
config: CoordinatorConfig,
nodes: Arc<RwLock<HashMap<String, NodeInfo>>>,
tasks: Arc<RwLock<Vec<EvaluationTask>>>,
results: Arc<RwLock<HashMap<String, Vec<NodeEvaluationResult>>>>,
}
impl FederatedCoordinator {
pub async fn new(config: CoordinatorConfig) -> Result<Self, FederatedError> {
Ok(Self {
config,
nodes: Arc::new(RwLock::new(HashMap::new())),
tasks: Arc::new(RwLock::new(Vec::new())),
results: Arc::new(RwLock::new(HashMap::new())),
})
}
pub async fn register_node(&self, node_id: &str, endpoint: &str) -> Result<(), FederatedError> {
let mut nodes = self.nodes.write().await;
if nodes.len() >= self.config.max_nodes {
return Err(FederatedError::RegistrationError {
message: format!("Maximum node limit reached: {}", self.config.max_nodes),
});
}
let info = NodeInfo {
node_id: node_id.to_string(),
endpoint: endpoint.to_string(),
status: NodeStatus::Online,
capabilities: NodeCapabilities::default(),
last_heartbeat: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("value should be present")
.as_secs(),
total_evaluations: 0,
current_load: 0.0,
};
nodes.insert(node_id.to_string(), info);
info!("Registered node: {}", node_id);
Ok(())
}
pub async fn unregister_node(&self, node_id: &str) -> Result<(), FederatedError> {
let mut nodes = self.nodes.write().await;
nodes.remove(node_id);
info!("Unregistered node: {}", node_id);
Ok(())
}
pub async fn active_node_count(&self) -> usize {
let nodes = self.nodes.read().await;
nodes
.values()
.filter(|n| n.status == NodeStatus::Online)
.count()
}
pub async fn create_task(&self, task_type: String) -> Result<String, FederatedError> {
let task_id = uuid::Uuid::new_v4().to_string();
let task = EvaluationTask {
task_id: task_id.clone(),
task_type,
parameters: HashMap::new(),
priority: 1,
created_at: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("value should be present")
.as_secs(),
};
let mut tasks = self.tasks.write().await;
tasks.push(task);
Ok(task_id)
}
pub async fn evaluate_federated(&self) -> Result<FederatedEvaluationResult, FederatedError> {
let nodes = self.nodes.read().await;
if nodes.len() < self.config.min_nodes {
return Err(FederatedError::AggregationError {
message: format!(
"Insufficient nodes: {} < {}",
nodes.len(),
self.config.min_nodes
),
});
}
let task_id = uuid::Uuid::new_v4().to_string();
let node_results: Vec<NodeEvaluationResult> = nodes
.iter()
.map(|(node_id, _)| NodeEvaluationResult {
node_id: node_id.clone(),
task_id: task_id.clone(),
quality_score: 4.2,
private_result: None,
duration_ms: 1000,
weight: 1.0,
completed_at: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("value should be present")
.as_secs(),
})
.collect();
let aggregated_score = self.aggregate_results(&node_results).await?;
let confidence_interval = self.calculate_confidence_interval(&node_results);
Ok(FederatedEvaluationResult {
task_id,
aggregated_score,
confidence_interval,
participant_count: node_results.len(),
node_results,
aggregation_strategy: self.config.aggregation_strategy,
total_duration_ms: 1000,
privacy_guarantees: vec!["Differential privacy enabled".to_string()],
})
}
async fn aggregate_results(
&self,
results: &[NodeEvaluationResult],
) -> Result<f64, FederatedError> {
if results.is_empty() {
return Err(FederatedError::AggregationError {
message: "No results to aggregate".to_string(),
});
}
let score = match self.config.aggregation_strategy {
AggregationStrategy::Average => {
results.iter().map(|r| r.quality_score).sum::<f64>() / results.len() as f64
}
AggregationStrategy::WeightedAverage => {
let total_weight: f64 = results.iter().map(|r| r.weight).sum();
results
.iter()
.map(|r| r.quality_score * r.weight)
.sum::<f64>()
/ total_weight
}
AggregationStrategy::Median => {
let mut scores: Vec<f64> = results.iter().map(|r| r.quality_score).collect();
scores.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let mid = scores.len() / 2;
if scores.len().is_multiple_of(2) {
(scores[mid - 1] + scores[mid]) / 2.0
} else {
scores[mid]
}
}
AggregationStrategy::TrimmedMean => {
let mut scores: Vec<f64> = results.iter().map(|r| r.quality_score).collect();
scores.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let trim_count = (scores.len() as f64 * 0.1) as usize; if scores.len() > 2 * trim_count {
let trimmed: Vec<f64> = scores
.iter()
.skip(trim_count)
.take(scores.len() - 2 * trim_count)
.copied()
.collect();
trimmed.iter().sum::<f64>() / trimmed.len() as f64
} else {
scores.iter().sum::<f64>() / scores.len() as f64
}
}
AggregationStrategy::FederatedLearning => {
results.iter().map(|r| r.quality_score).sum::<f64>() / results.len() as f64
}
};
Ok(score)
}
fn calculate_confidence_interval(&self, results: &[NodeEvaluationResult]) -> (f64, f64) {
if results.is_empty() {
return (0.0, 0.0);
}
let scores: Vec<f64> = results.iter().map(|r| r.quality_score).collect();
let mean = scores.iter().sum::<f64>() / scores.len() as f64;
let variance = scores.iter().map(|s| (s - mean).powi(2)).sum::<f64>() / scores.len() as f64;
let std_dev = variance.sqrt();
let margin = 1.96 * std_dev / (scores.len() as f64).sqrt();
((mean - margin).max(0.0), (mean + margin).min(5.0))
}
pub async fn get_statistics(&self) -> CoordinatorStatistics {
let nodes = self.nodes.read().await;
let tasks = self.tasks.read().await;
CoordinatorStatistics {
total_nodes: nodes.len(),
active_nodes: nodes
.values()
.filter(|n| n.status == NodeStatus::Online)
.count(),
total_tasks: tasks.len(),
completed_tasks: 0, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoordinatorStatistics {
pub total_nodes: usize,
pub active_nodes: usize,
pub total_tasks: usize,
pub completed_tasks: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_node_config_default() {
let config = NodeConfig::default();
assert_eq!(config.node_id, "default_node");
assert!(config.enable_privacy);
assert_eq!(config.max_concurrent_evaluations, 4);
}
#[test]
fn test_coordinator_config_default() {
let config = CoordinatorConfig::default();
assert_eq!(config.min_nodes, 2);
assert_eq!(config.max_nodes, 100);
assert_eq!(config.consensus_threshold, 0.67);
}
#[test]
fn test_node_capabilities_default() {
let caps = NodeCapabilities::default();
assert!(caps.metrics.contains(&"pesq".to_string()));
assert_eq!(caps.max_concurrent, 4);
assert_eq!(caps.speed_factor, 1.0);
}
#[test]
fn test_aggregation_strategies() {
assert_eq!(AggregationStrategy::Average, AggregationStrategy::Average);
assert_ne!(AggregationStrategy::Average, AggregationStrategy::Median);
}
#[tokio::test]
async fn test_coordinator_creation() {
let config = CoordinatorConfig::default();
let coordinator = FederatedCoordinator::new(config).await;
assert!(coordinator.is_ok());
}
#[tokio::test]
async fn test_node_registration() {
let config = CoordinatorConfig::default();
let coordinator = FederatedCoordinator::new(config).await.unwrap();
coordinator
.register_node("node1", "http://node1:8080")
.await
.unwrap();
let count = coordinator.active_node_count().await;
assert_eq!(count, 1);
coordinator
.register_node("node2", "http://node2:8080")
.await
.unwrap();
let count = coordinator.active_node_count().await;
assert_eq!(count, 2);
}
#[tokio::test]
async fn test_node_unregistration() {
let config = CoordinatorConfig::default();
let coordinator = FederatedCoordinator::new(config).await.unwrap();
coordinator
.register_node("node1", "http://node1:8080")
.await
.unwrap();
coordinator.unregister_node("node1").await.unwrap();
let count = coordinator.active_node_count().await;
assert_eq!(count, 0);
}
#[tokio::test]
async fn test_task_creation() {
let config = CoordinatorConfig::default();
let coordinator = FederatedCoordinator::new(config).await.unwrap();
let task_id = coordinator
.create_task("quality_evaluation".to_string())
.await
.unwrap();
assert!(!task_id.is_empty());
}
#[tokio::test]
async fn test_coordinator_statistics() {
let config = CoordinatorConfig::default();
let coordinator = FederatedCoordinator::new(config).await.unwrap();
coordinator
.register_node("node1", "http://node1:8080")
.await
.unwrap();
coordinator
.register_node("node2", "http://node2:8080")
.await
.unwrap();
let stats = coordinator.get_statistics().await;
assert_eq!(stats.total_nodes, 2);
assert_eq!(stats.active_nodes, 2);
}
#[tokio::test]
async fn test_federated_node_creation() {
let config = NodeConfig::default();
let node = FederatedNode::new(config).await;
assert!(node.is_ok());
}
#[tokio::test]
async fn test_node_heartbeat() {
let config = NodeConfig::default();
let node = FederatedNode::new(config).await.unwrap();
let info_before = node.get_info().await;
tokio::time::sleep(Duration::from_secs(1)).await;
node.heartbeat().await;
let info_after = node.get_info().await;
assert!(info_after.last_heartbeat >= info_before.last_heartbeat);
}
}