Skip to main content

sklears_core/
distributed.rs

1/// Distributed computing infrastructure for sklears-core
2///
3/// This module provides comprehensive distributed computing capabilities for machine learning
4/// workloads, including message-passing, cluster-aware estimators, distributed datasets,
5/// and fault-tolerant training frameworks.
6///
7/// # Key Features
8///
9/// - **Message Passing**: Efficient communication primitives for cluster nodes
10/// - **Distributed Estimators**: ML algorithms that scale across multiple nodes
11/// - **Partitioned Datasets**: Data structures optimized for distributed processing
12/// - **Fault Tolerance**: Automatic recovery and checkpoint management
13/// - **Load Balancing**: Dynamic work distribution across cluster nodes
14/// - **Consistency Models**: Eventual and strong consistency guarantees
15///
16/// # Architecture
17///
18/// The distributed computing system is built around several core abstractions:
19///
20/// ## Node Communication
21/// ```rust,ignore
22/// use sklears_core::distributed::{MessagePassing, ClusterNode, NodeId};
23///
24/// // Basic message passing between cluster nodes
25/// async fn example_communication(node: &dyn ClusterNode) -> Result<(), Box<dyn std::error::Error>> {
26///     let target_node = NodeId::new("worker-01");
27///     let message = b"training_data_chunk_1";
28///
29///     node.send_message(target_node, message).await?;
30///     let response = node.receive_message().await?;
31///
32///     Ok(())
33/// }
34/// ```
35///
36/// ## Distributed Training
37/// ```rust,ignore
38/// use sklears_core::distributed::{DistributedEstimator, ParameterServer};
39///
40/// // Distributed machine learning with parameter server architecture
41/// async fn example_distributed_training() -> Result<(), Box<dyn std::error::Error>> {
42///     let cluster = DistributedCluster::new()
43///         .with_nodes(4)
44///         .with_parameter_server()
45///         .build().await?;
46///
47///     let model = DistributedLinearRegression::new()
48///         .with_cluster(cluster)
49///         .with_fault_tolerance(true)
50///         .build();
51///
52///     // Training automatically distributes across cluster
53///     model.fit_distributed(&X_train, &y_train).await?;
54///
55///     Ok(())
56/// }
57/// ```
58use crate::error::{Result, SklearsError};
59use futures_core::future::BoxFuture;
60use serde::{Deserialize, Serialize};
61use std::collections::HashMap;
62use std::sync::{Arc, RwLock};
63use std::time::{Duration, SystemTime};
64
65// =============================================================================
66// Core Distributed Computing Traits
67// =============================================================================
68
69/// Unique identifier for cluster nodes
70#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
71pub struct NodeId(pub String);
72
73impl NodeId {
74    /// Create a new node identifier
75    pub fn new(id: impl Into<String>) -> Self {
76        Self(id.into())
77    }
78
79    /// Get the string representation of the node ID
80    pub fn as_str(&self) -> &str {
81        &self.0
82    }
83}
84
85impl std::fmt::Display for NodeId {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        write!(f, "{}", self.0)
88    }
89}
90
91/// Message envelope for inter-node communication
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct DistributedMessage {
94    /// Unique message identifier
95    pub id: String,
96    /// Source node identifier
97    pub sender: NodeId,
98    /// Target node identifier
99    pub receiver: NodeId,
100    /// Message type classification
101    pub message_type: MessageType,
102    /// Actual message payload
103    pub payload: Vec<u8>,
104    /// Message timestamp
105    pub timestamp: SystemTime,
106    /// Message priority level
107    pub priority: MessagePriority,
108    /// Retry count for fault tolerance
109    pub retry_count: u32,
110}
111
112/// Classification of distributed messages
113#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
114pub enum MessageType {
115    /// Data transfer between nodes
116    DataTransfer,
117    /// Model parameter synchronization
118    ParameterSync,
119    /// Gradient aggregation
120    GradientAggregation,
121    /// Cluster coordination
122    Coordination,
123    /// Health check and monitoring
124    HealthCheck,
125    /// Fault recovery
126    FaultRecovery,
127    /// Load balancing
128    LoadBalance,
129    /// Custom application-specific messages
130    Custom(String),
131}
132
133/// Message priority levels for scheduling
134#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
135pub enum MessagePriority {
136    /// Low priority background tasks
137    Low = 0,
138    /// Normal operation messages
139    Normal = 1,
140    /// High priority coordination
141    High = 2,
142    /// Critical system messages
143    Critical = 3,
144}
145
146/// Core trait for message-passing communication in distributed systems
147pub trait MessagePassing: Send + Sync {
148    /// Send a message to a specific node
149    fn send_message(
150        &self,
151        target: NodeId,
152        message: DistributedMessage,
153    ) -> BoxFuture<'_, Result<()>>;
154
155    /// Receive the next available message
156    fn receive_message(&self) -> BoxFuture<'_, Result<DistributedMessage>>;
157
158    /// Broadcast a message to all nodes in the cluster
159    fn broadcast_message(&self, message: DistributedMessage) -> BoxFuture<'_, Result<()>>;
160
161    /// Send a message and wait for a response
162    fn send_and_receive(
163        &self,
164        target: NodeId,
165        message: DistributedMessage,
166    ) -> BoxFuture<'_, Result<DistributedMessage>>;
167
168    /// Check if any messages are available
169    fn has_pending_messages(&self) -> BoxFuture<'_, Result<bool>>;
170
171    /// Get the number of pending messages
172    fn pending_message_count(&self) -> BoxFuture<'_, Result<usize>>;
173
174    /// Flush all pending outgoing messages
175    fn flush_outgoing(&self) -> BoxFuture<'_, Result<()>>;
176}
177
178/// Cluster node abstraction for distributed computing
179pub trait ClusterNode: MessagePassing + Send + Sync {
180    /// Get the unique identifier for this node
181    fn node_id(&self) -> &NodeId;
182
183    /// Get the current cluster membership
184    fn cluster_nodes(&self) -> BoxFuture<'_, Result<Vec<NodeId>>>;
185
186    /// Check if this node is the cluster coordinator
187    fn is_coordinator(&self) -> bool;
188
189    /// Get current node health status
190    fn health_status(&self) -> BoxFuture<'_, Result<NodeHealth>>;
191
192    /// Get node computational resources
193    fn resources(&self) -> BoxFuture<'_, Result<NodeResources>>;
194
195    /// Join a cluster
196    fn join_cluster(&mut self, coordinator: NodeId) -> BoxFuture<'_, Result<()>>;
197
198    /// Leave the current cluster
199    fn leave_cluster(&mut self) -> BoxFuture<'_, Result<()>>;
200
201    /// Handle node failure detection
202    fn handle_node_failure(&mut self, failed_node: NodeId) -> BoxFuture<'_, Result<()>>;
203}
204
205/// Node health status information
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct NodeHealth {
208    /// Overall health score (0.0 to 1.0)
209    pub health_score: f64,
210    /// CPU utilization percentage
211    pub cpu_usage: f64,
212    /// Memory utilization percentage
213    pub memory_usage: f64,
214    /// Network latency to coordinator (ms)
215    pub network_latency: Duration,
216    /// Last heartbeat timestamp
217    pub last_heartbeat: SystemTime,
218    /// Error count in last hour
219    pub recent_errors: u32,
220    /// Node uptime
221    pub uptime: Duration,
222}
223
224/// Node computational resources
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct NodeResources {
227    /// Number of CPU cores
228    pub cpu_cores: u32,
229    /// Total memory in bytes
230    pub total_memory: u64,
231    /// Available memory in bytes
232    pub available_memory: u64,
233    /// GPU devices available
234    pub gpu_devices: Vec<GpuDevice>,
235    /// Network bandwidth (bytes/sec)
236    pub network_bandwidth: u64,
237    /// Storage capacity in bytes
238    pub storage_capacity: u64,
239    /// Custom resource tags
240    pub tags: HashMap<String, String>,
241}
242
243/// GPU device information
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct GpuDevice {
246    /// Device identifier
247    pub device_id: u32,
248    /// Device name/model
249    pub name: String,
250    /// Total VRAM in bytes
251    pub total_memory: u64,
252    /// Available VRAM in bytes
253    pub available_memory: u64,
254    /// Compute capability
255    pub compute_capability: String,
256}
257
258// =============================================================================
259// Distributed Estimator Framework
260// =============================================================================
261
262/// Core trait for distributed machine learning estimators
263pub trait DistributedEstimator: Send + Sync {
264    /// Associated type for training data
265    type TrainingData;
266
267    /// Associated type for prediction input
268    type PredictionInput;
269
270    /// Associated type for prediction output
271    type PredictionOutput;
272
273    /// Associated type for model parameters
274    type Parameters: Serialize + for<'de> Deserialize<'de>;
275
276    /// Fit the model using distributed training
277    fn fit_distributed<'a>(
278        &'a mut self,
279        cluster: &'a dyn DistributedCluster,
280        training_data: &Self::TrainingData,
281    ) -> BoxFuture<'a, Result<()>>;
282
283    /// Make predictions using the distributed model
284    fn predict_distributed<'a>(
285        &'a self,
286        cluster: &dyn DistributedCluster,
287        input: &'a Self::PredictionInput,
288    ) -> BoxFuture<'a, Result<Self::PredictionOutput>>;
289
290    /// Get current model parameters
291    fn get_parameters(&self) -> Result<Self::Parameters>;
292
293    /// Set model parameters
294    fn set_parameters(&mut self, params: Self::Parameters) -> Result<()>;
295
296    /// Synchronize parameters across cluster nodes
297    fn sync_parameters(&mut self, cluster: &dyn DistributedCluster) -> BoxFuture<'_, Result<()>>;
298
299    /// Get training progress information
300    fn training_progress(&self) -> DistributedTrainingProgress;
301}
302
303/// Progress tracking for distributed training
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct DistributedTrainingProgress {
306    /// Current epoch number
307    pub epoch: u32,
308    /// Total epochs planned
309    pub total_epochs: u32,
310    /// Training loss value
311    pub training_loss: f64,
312    /// Validation loss value
313    pub validation_loss: Option<f64>,
314    /// Number of samples processed
315    pub samples_processed: u64,
316    /// Training start time
317    pub start_time: SystemTime,
318    /// Estimated completion time
319    pub estimated_completion: Option<SystemTime>,
320    /// Active cluster nodes
321    pub active_nodes: Vec<NodeId>,
322    /// Per-node training statistics
323    pub node_statistics: HashMap<NodeId, NodeTrainingStats>,
324}
325
326/// Training statistics for individual nodes
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct NodeTrainingStats {
329    /// Samples processed by this node
330    pub samples_processed: u64,
331    /// Processing rate (samples/sec)
332    pub processing_rate: f64,
333    /// Current loss value
334    pub current_loss: f64,
335    /// Memory usage during training
336    pub memory_usage: u64,
337    /// CPU utilization during training
338    pub cpu_utilization: f64,
339}
340
341/// Distributed cluster management interface
342pub trait DistributedCluster: Send + Sync {
343    /// Get all active nodes in the cluster
344    fn active_nodes(&self) -> BoxFuture<'_, Result<Vec<NodeId>>>;
345
346    /// Get the cluster coordinator node
347    fn coordinator(&self) -> &NodeId;
348
349    /// Get cluster configuration
350    fn configuration(&self) -> &ClusterConfiguration;
351
352    /// Add a new node to the cluster
353    fn add_node(&mut self, node: NodeId) -> BoxFuture<'_, Result<()>>;
354
355    /// Remove a node from the cluster
356    fn remove_node(&mut self, node: NodeId) -> BoxFuture<'_, Result<()>>;
357
358    /// Redistribute work across cluster nodes
359    fn rebalance_load(&mut self) -> BoxFuture<'_, Result<()>>;
360
361    /// Get cluster health status
362    fn cluster_health(&self) -> BoxFuture<'_, Result<ClusterHealth>>;
363
364    /// Create a checkpoint of cluster state
365    fn create_checkpoint(&self) -> BoxFuture<'_, Result<ClusterCheckpoint>>;
366
367    /// Restore from a checkpoint
368    fn restore_checkpoint(&mut self, checkpoint: ClusterCheckpoint) -> BoxFuture<'_, Result<()>>;
369}
370
371/// Cluster configuration parameters
372#[derive(Debug, Clone, Serialize, Deserialize)]
373pub struct ClusterConfiguration {
374    /// Maximum number of nodes
375    pub max_nodes: u32,
376    /// Heartbeat interval
377    pub heartbeat_interval: Duration,
378    /// Node failure timeout
379    pub failure_timeout: Duration,
380    /// Message retry limit
381    pub max_retries: u32,
382    /// Load balancing strategy
383    pub load_balancing: LoadBalancingStrategy,
384    /// Fault tolerance mode
385    pub fault_tolerance: FaultToleranceMode,
386    /// Consistency requirements
387    pub consistency_level: ConsistencyLevel,
388}
389
390/// Load balancing strategies
391#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
392pub enum LoadBalancingStrategy {
393    /// Round-robin assignment
394    RoundRobin,
395    /// Assign based on node resources
396    ResourceBased,
397    /// Assign based on current load
398    LoadBased,
399    /// Assign based on data locality
400    LocalityAware,
401    /// Custom balancing strategy
402    Custom(String),
403}
404
405/// Fault tolerance modes
406#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
407pub enum FaultToleranceMode {
408    /// No fault tolerance
409    None,
410    /// Basic retry mechanisms
411    BasicRetry,
412    /// Checkpoint-based recovery
413    CheckpointRecovery,
414    /// Redundant computation
415    RedundantComputation,
416    /// Byzantine fault tolerance
417    Byzantine,
418}
419
420/// Consistency levels for distributed operations
421#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
422pub enum ConsistencyLevel {
423    /// No consistency guarantees
424    None,
425    /// Eventually consistent
426    Eventual,
427    /// Strong consistency
428    Strong,
429    /// Causal consistency
430    Causal,
431    /// Sequential consistency
432    Sequential,
433}
434
435/// Overall cluster health information
436#[derive(Debug, Clone, Serialize, Deserialize)]
437pub struct ClusterHealth {
438    /// Overall cluster health score
439    pub overall_health: f64,
440    /// Number of healthy nodes
441    pub healthy_nodes: u32,
442    /// Number of failed nodes
443    pub failed_nodes: u32,
444    /// Average node response time
445    pub average_response_time: Duration,
446    /// Total cluster throughput
447    pub total_throughput: f64,
448    /// Resource utilization across cluster
449    pub resource_utilization: ClusterResourceUtilization,
450}
451
452/// Cluster-wide resource utilization
453#[derive(Debug, Clone, Serialize, Deserialize)]
454pub struct ClusterResourceUtilization {
455    /// Average CPU utilization
456    pub cpu_utilization: f64,
457    /// Average memory utilization
458    pub memory_utilization: f64,
459    /// Network utilization
460    pub network_utilization: f64,
461    /// Storage utilization
462    pub storage_utilization: f64,
463}
464
465/// Cluster state checkpoint for fault recovery
466#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct ClusterCheckpoint {
468    /// Checkpoint identifier
469    pub checkpoint_id: String,
470    /// Checkpoint timestamp
471    pub timestamp: SystemTime,
472    /// Cluster configuration at checkpoint time
473    pub configuration: ClusterConfiguration,
474    /// Node states at checkpoint time
475    pub node_states: HashMap<NodeId, NodeCheckpoint>,
476    /// Global cluster state
477    pub cluster_state: Vec<u8>,
478}
479
480/// Individual node checkpoint data
481#[derive(Debug, Clone, Serialize, Deserialize)]
482pub struct NodeCheckpoint {
483    /// Node identifier
484    pub node_id: NodeId,
485    /// Node state data
486    pub state_data: Vec<u8>,
487    /// Node health at checkpoint time
488    pub health: NodeHealth,
489    /// Node resources at checkpoint time
490    pub resources: NodeResources,
491}
492
493// =============================================================================
494// Distributed Dataset Abstractions
495// =============================================================================
496
497/// Trait for datasets that can be distributed across cluster nodes
498pub trait DistributedDataset: Send + Sync {
499    /// Associated type for data items
500    type Item;
501
502    /// Associated type for partitioning strategy
503    type PartitionStrategy;
504
505    /// Get the total size of the dataset
506    fn size(&self) -> u64;
507
508    /// Get the number of partitions
509    fn partition_count(&self) -> u32;
510
511    /// Partition the dataset across cluster nodes
512    fn partition<'a>(
513        &'a mut self,
514        cluster: &'a dyn DistributedCluster,
515        strategy: Self::PartitionStrategy,
516    ) -> BoxFuture<'a, Result<Vec<DistributedPartition<Self::Item>>>>;
517
518    /// Get a specific partition
519    fn get_partition(
520        &self,
521        partition_id: u32,
522    ) -> BoxFuture<'_, Result<DistributedPartition<Self::Item>>>;
523
524    /// Repartition the dataset with a new strategy
525    fn repartition<'a>(
526        &'a mut self,
527        cluster: &'a dyn DistributedCluster,
528        new_strategy: Self::PartitionStrategy,
529    ) -> BoxFuture<'a, Result<()>>;
530
531    /// Collect all partitions back to coordinator
532    fn collect(&self, cluster: &dyn DistributedCluster) -> BoxFuture<'_, Result<Vec<Self::Item>>>;
533
534    /// Get partition assignment for nodes
535    fn partition_assignment(&self) -> HashMap<NodeId, Vec<u32>>;
536}
537
538/// A partition of a distributed dataset
539#[derive(Debug, Clone)]
540pub struct DistributedPartition<T> {
541    /// Partition identifier
542    pub partition_id: u32,
543    /// Node holding this partition
544    pub node_id: NodeId,
545    /// Partition data
546    pub data: Vec<T>,
547    /// Partition metadata
548    pub metadata: PartitionMetadata,
549}
550
551/// Metadata about a data partition
552#[derive(Debug, Clone, Serialize, Deserialize)]
553pub struct PartitionMetadata {
554    /// Number of items in partition
555    pub item_count: u64,
556    /// Partition size in bytes
557    pub size_bytes: u64,
558    /// Data schema information
559    pub schema: Option<String>,
560    /// Partition creation timestamp
561    pub created_at: SystemTime,
562    /// Last modification timestamp
563    pub modified_at: SystemTime,
564    /// Checksum for integrity verification
565    pub checksum: String,
566}
567
568/// Partitioning strategies for distributed datasets
569#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
570pub enum PartitioningStrategy {
571    /// Split data evenly across nodes
572    EvenSplit,
573    /// Partition based on data hash
574    HashBased(u32),
575    /// Partition based on data ranges
576    RangeBased,
577    /// Random partitioning
578    Random,
579    /// Stratified partitioning (for classification)
580    Stratified,
581    /// Custom partitioning function
582    Custom(String),
583}
584
585// =============================================================================
586// Parameter Server Architecture
587// =============================================================================
588
589/// Parameter server for coordinating distributed machine learning
590pub trait ParameterServer: Send + Sync {
591    /// Associated type for parameters
592    type Parameters: Serialize + for<'de> Deserialize<'de>;
593
594    /// Initialize the parameter server
595    fn initialize(&mut self, initial_params: Self::Parameters) -> BoxFuture<'_, Result<()>>;
596
597    /// Get current parameters
598    fn get_parameters(&self) -> BoxFuture<'_, Result<Self::Parameters>>;
599
600    /// Update parameters with gradients
601    fn update_parameters(&mut self, gradients: Vec<Self::Parameters>) -> BoxFuture<'_, Result<()>>;
602
603    /// Push parameters to all worker nodes
604    fn push_parameters(&self, cluster: &dyn DistributedCluster) -> BoxFuture<'_, Result<()>>;
605
606    /// Pull parameters from worker nodes
607    fn pull_parameters(&mut self, cluster: &dyn DistributedCluster) -> BoxFuture<'_, Result<()>>;
608
609    /// Aggregate gradients from worker nodes
610    fn aggregate_gradients(
611        &mut self,
612        gradients: Vec<Self::Parameters>,
613    ) -> BoxFuture<'_, Result<Self::Parameters>>;
614
615    /// Apply learning rate and optimization
616    fn apply_optimization(
617        &mut self,
618        aggregated_gradients: Self::Parameters,
619    ) -> BoxFuture<'_, Result<()>>;
620}
621
622/// Gradient aggregation strategies
623#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
624pub enum GradientAggregation {
625    /// Simple averaging
626    Average,
627    /// Weighted averaging by node resources
628    WeightedAverage,
629    /// Federated averaging with decay
630    FederatedAveraging,
631    /// Byzantine-robust aggregation
632    ByzantineRobust,
633    /// Compression-based aggregation
634    Compressed,
635}
636
637// =============================================================================
638// Fault Tolerance Framework
639// =============================================================================
640
641/// Comprehensive fault tolerance system for distributed training
642pub trait FaultTolerance: Send + Sync {
643    /// Detect when a node has failed
644    fn detect_failure(
645        &self,
646        cluster: &dyn DistributedCluster,
647    ) -> BoxFuture<'_, Result<Vec<NodeId>>>;
648
649    /// Recover from node failures
650    fn recover_from_failure(
651        &mut self,
652        cluster: &mut dyn DistributedCluster,
653        failed_nodes: Vec<NodeId>,
654    ) -> BoxFuture<'_, Result<()>>;
655
656    /// Create a checkpoint for recovery
657    fn create_checkpoint(
658        &self,
659        cluster: &dyn DistributedCluster,
660    ) -> BoxFuture<'_, Result<FaultToleranceCheckpoint>>;
661
662    /// Restore from a checkpoint
663    fn restore_checkpoint(
664        &mut self,
665        cluster: &mut dyn DistributedCluster,
666        checkpoint: FaultToleranceCheckpoint,
667    ) -> BoxFuture<'_, Result<()>>;
668
669    /// Replicate critical data across nodes
670    fn replicate_data(
671        &self,
672        cluster: &dyn DistributedCluster,
673        data: Vec<u8>,
674    ) -> BoxFuture<'_, Result<()>>;
675
676    /// Validate cluster integrity
677    fn validate_integrity(
678        &self,
679        cluster: &dyn DistributedCluster,
680    ) -> BoxFuture<'_, Result<IntegrityReport>>;
681}
682
683/// Checkpoint data for fault tolerance
684#[derive(Debug, Clone, Serialize, Deserialize)]
685pub struct FaultToleranceCheckpoint {
686    /// Checkpoint identifier
687    pub id: String,
688    /// Checkpoint timestamp
689    pub timestamp: SystemTime,
690    /// Training state at checkpoint
691    pub training_state: Vec<u8>,
692    /// Model parameters at checkpoint
693    pub model_parameters: Vec<u8>,
694    /// Node assignments at checkpoint
695    pub node_assignments: HashMap<NodeId, Vec<u32>>,
696    /// Replication information
697    pub replication_map: HashMap<String, Vec<NodeId>>,
698}
699
700/// Cluster integrity validation report
701#[derive(Debug, Clone, Serialize, Deserialize)]
702pub struct IntegrityReport {
703    /// Overall integrity score
704    pub integrity_score: f64,
705    /// Data consistency validation
706    pub data_consistency: bool,
707    /// Parameter synchronization status
708    pub parameter_sync: bool,
709    /// Replication health
710    pub replication_health: f64,
711    /// Detected inconsistencies
712    pub inconsistencies: Vec<String>,
713    /// Recommended actions
714    pub recommendations: Vec<String>,
715}
716
717// =============================================================================
718// Concrete Implementations
719// =============================================================================
720
721/// Default implementation of a distributed cluster
722pub struct DefaultDistributedCluster {
723    /// Cluster configuration
724    configuration: ClusterConfiguration,
725    /// Coordinator node
726    coordinator: NodeId,
727    /// Active cluster nodes
728    nodes: Arc<RwLock<HashMap<NodeId, Arc<dyn ClusterNode>>>>,
729    /// Cluster health monitoring
730    health_monitor: Arc<RwLock<ClusterHealth>>,
731}
732
733impl std::fmt::Debug for DefaultDistributedCluster {
734    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
735        f.debug_struct("DefaultDistributedCluster")
736            .field("configuration", &self.configuration)
737            .field("coordinator", &self.coordinator)
738            .field("nodes", &"<HashMap<NodeId, Arc<dyn ClusterNode>>>")
739            .field("health_monitor", &self.health_monitor)
740            .finish()
741    }
742}
743
744impl DefaultDistributedCluster {
745    /// Create a new distributed cluster
746    pub fn new(coordinator: NodeId, configuration: ClusterConfiguration) -> Self {
747        Self {
748            configuration,
749            coordinator,
750            nodes: Arc::new(RwLock::new(HashMap::new())),
751            health_monitor: Arc::new(RwLock::new(ClusterHealth {
752                overall_health: 1.0,
753                healthy_nodes: 0,
754                failed_nodes: 0,
755                average_response_time: Duration::from_millis(10),
756                total_throughput: 0.0,
757                resource_utilization: ClusterResourceUtilization {
758                    cpu_utilization: 0.0,
759                    memory_utilization: 0.0,
760                    network_utilization: 0.0,
761                    storage_utilization: 0.0,
762                },
763            })),
764        }
765    }
766}
767
768impl DistributedCluster for DefaultDistributedCluster {
769    fn active_nodes(&self) -> BoxFuture<'_, Result<Vec<NodeId>>> {
770        Box::pin(async move {
771            let nodes = self.nodes.read().map_err(|_| {
772                SklearsError::InvalidOperation("Failed to acquire read lock on nodes".to_string())
773            })?;
774            Ok(nodes.keys().cloned().collect())
775        })
776    }
777
778    fn coordinator(&self) -> &NodeId {
779        &self.coordinator
780    }
781
782    fn configuration(&self) -> &ClusterConfiguration {
783        &self.configuration
784    }
785
786    fn add_node(&mut self, _node_id: NodeId) -> BoxFuture<'_, Result<()>> {
787        Box::pin(async move {
788            // Implementation would add the node to the cluster
789            // For now, this is a placeholder
790            Ok(())
791        })
792    }
793
794    fn remove_node(&mut self, node_id: NodeId) -> BoxFuture<'_, Result<()>> {
795        Box::pin(async move {
796            let mut nodes = self.nodes.write().map_err(|_| {
797                SklearsError::InvalidOperation("Failed to acquire write lock on nodes".to_string())
798            })?;
799            nodes.remove(&node_id);
800            Ok(())
801        })
802    }
803
804    fn rebalance_load(&mut self) -> BoxFuture<'_, Result<()>> {
805        Box::pin(async move {
806            // Implementation would redistribute work based on current load
807            Ok(())
808        })
809    }
810
811    fn cluster_health(&self) -> BoxFuture<'_, Result<ClusterHealth>> {
812        Box::pin(async move {
813            let health = self.health_monitor.read().map_err(|_| {
814                SklearsError::InvalidOperation(
815                    "Failed to acquire read lock on health monitor".to_string(),
816                )
817            })?;
818            Ok(health.clone())
819        })
820    }
821
822    fn create_checkpoint(&self) -> BoxFuture<'_, Result<ClusterCheckpoint>> {
823        Box::pin(async move {
824            let checkpoint = ClusterCheckpoint {
825                checkpoint_id: format!("checkpoint_{}", chrono::Utc::now().timestamp()),
826                timestamp: SystemTime::now(),
827                configuration: self.configuration.clone(),
828                node_states: HashMap::new(), // Would collect actual node states
829                cluster_state: Vec::new(),   // Would serialize cluster state
830            };
831            Ok(checkpoint)
832        })
833    }
834
835    fn restore_checkpoint(&mut self, _checkpoint: ClusterCheckpoint) -> BoxFuture<'_, Result<()>> {
836        Box::pin(async move {
837            // Implementation would restore cluster state from checkpoint
838            Ok(())
839        })
840    }
841}
842
843impl Default for ClusterConfiguration {
844    fn default() -> Self {
845        Self {
846            max_nodes: 64,
847            heartbeat_interval: Duration::from_secs(30),
848            failure_timeout: Duration::from_secs(120),
849            max_retries: 3,
850            load_balancing: LoadBalancingStrategy::ResourceBased,
851            fault_tolerance: FaultToleranceMode::CheckpointRecovery,
852            consistency_level: ConsistencyLevel::Eventual,
853        }
854    }
855}
856
857// =============================================================================
858// Example Distributed Estimator Implementation
859// =============================================================================
860
861/// Example distributed linear regression implementation
862#[derive(Debug)]
863pub struct DistributedLinearRegression {
864    /// Model parameters (weights and bias)
865    parameters: Option<Vec<f64>>,
866    /// Training configuration
867    config: DistributedTrainingConfig,
868    /// Training progress
869    progress: DistributedTrainingProgress,
870}
871
872/// Configuration for distributed training
873#[derive(Debug, Clone)]
874pub struct DistributedTrainingConfig {
875    /// Learning rate
876    pub learning_rate: f64,
877    /// Number of epochs
878    pub epochs: u32,
879    /// Batch size per node
880    pub batch_size: u32,
881    /// Gradient aggregation strategy
882    pub aggregation: GradientAggregation,
883    /// Checkpoint frequency
884    pub checkpoint_frequency: u32,
885}
886
887impl Default for DistributedLinearRegression {
888    fn default() -> Self {
889        Self::new()
890    }
891}
892
893impl DistributedLinearRegression {
894    /// Create a new distributed linear regression model
895    pub fn new() -> Self {
896        Self {
897            parameters: None,
898            config: DistributedTrainingConfig::default(),
899            progress: DistributedTrainingProgress {
900                epoch: 0,
901                total_epochs: 0,
902                training_loss: 0.0,
903                validation_loss: None,
904                samples_processed: 0,
905                start_time: SystemTime::now(),
906                estimated_completion: None,
907                active_nodes: Vec::new(),
908                node_statistics: HashMap::new(),
909            },
910        }
911    }
912
913    /// Configure the distributed training parameters
914    pub fn with_config(mut self, config: DistributedTrainingConfig) -> Self {
915        self.config = config;
916        self
917    }
918}
919
920impl Default for DistributedTrainingConfig {
921    fn default() -> Self {
922        Self {
923            learning_rate: 0.01,
924            epochs: 100,
925            batch_size: 32,
926            aggregation: GradientAggregation::Average,
927            checkpoint_frequency: 10,
928        }
929    }
930}
931
932impl DistributedEstimator for DistributedLinearRegression {
933    type TrainingData = (Vec<Vec<f64>>, Vec<f64>); // (X, y)
934    type PredictionInput = Vec<Vec<f64>>;
935    type PredictionOutput = Vec<f64>;
936    type Parameters = Vec<f64>;
937
938    fn fit_distributed<'a>(
939        &'a mut self,
940        _cluster: &'a dyn DistributedCluster,
941        training_data: &Self::TrainingData,
942    ) -> BoxFuture<'a, Result<()>> {
943        let training_data = training_data.clone();
944        Box::pin(async move {
945            let (x, _y) = &training_data;
946
947            // Initialize parameters if needed
948            if self.parameters.is_none() {
949                let feature_count = x.first().map(|row| row.len()).unwrap_or(0);
950                self.parameters = Some(vec![0.0; feature_count + 1]); // +1 for bias
951            }
952
953            // Set up training progress
954            self.progress.total_epochs = self.config.epochs;
955            self.progress.start_time = SystemTime::now();
956            self.progress.active_nodes = vec![]; // Simplified for now
957
958            // Simulate distributed training process
959            for epoch in 0..self.config.epochs {
960                self.progress.epoch = epoch;
961
962                // In a real implementation, this would:
963                // 1. Distribute data across nodes
964                // 2. Compute gradients on each node
965                // 3. Aggregate gradients using parameter server
966                // 4. Update parameters
967                // 5. Synchronize across cluster
968
969                // Placeholder implementation
970                if let Some(ref mut params) = self.parameters {
971                    // Simulate gradient descent step
972                    for param in params.iter_mut() {
973                        *param += self.config.learning_rate * 0.1; // Dummy gradient
974                    }
975                }
976
977                // Update progress
978                self.progress.samples_processed += x.len() as u64;
979                self.progress.training_loss = (epoch as f64 * 0.1).exp().recip(); // Decreasing loss
980
981                // Create checkpoint if needed
982                if epoch % self.config.checkpoint_frequency == 0 {
983                    // Simplified: Would create checkpoint in real implementation
984                    // let _checkpoint = cluster.create_checkpoint().await?;
985                }
986            }
987
988            Ok(())
989        })
990    }
991
992    fn predict_distributed<'a>(
993        &'a self,
994        _cluster: &dyn DistributedCluster,
995        input: &'a Self::PredictionInput,
996    ) -> BoxFuture<'a, Result<Self::PredictionOutput>> {
997        Box::pin(async move {
998            let Some(ref params) = self.parameters else {
999                return Err(SklearsError::InvalidOperation(
1000                    "Model not trained. Call fit_distributed first.".to_string(),
1001                ));
1002            };
1003
1004            // Simple linear prediction: X * weights + bias
1005            let predictions = input
1006                .iter()
1007                .map(|features| {
1008                    let mut prediction = *params.last().unwrap_or(&0.0); // bias term
1009                    for (feature, weight) in features.iter().zip(params.iter()) {
1010                        prediction += feature * weight;
1011                    }
1012                    prediction
1013                })
1014                .collect();
1015
1016            Ok(predictions)
1017        })
1018    }
1019
1020    fn get_parameters(&self) -> Result<Self::Parameters> {
1021        self.parameters
1022            .clone()
1023            .ok_or_else(|| SklearsError::InvalidOperation("Model not trained".to_string()))
1024    }
1025
1026    fn set_parameters(&mut self, params: Self::Parameters) -> Result<()> {
1027        self.parameters = Some(params);
1028        Ok(())
1029    }
1030
1031    fn sync_parameters(&mut self, _cluster: &dyn DistributedCluster) -> BoxFuture<'_, Result<()>> {
1032        Box::pin(async move {
1033            // Implementation would synchronize parameters across all cluster nodes
1034            Ok(())
1035        })
1036    }
1037
1038    fn training_progress(&self) -> DistributedTrainingProgress {
1039        self.progress.clone()
1040    }
1041}
1042
1043// =============================================================================
1044// Distributed Dataset Implementation
1045// =============================================================================
1046
1047/// Example distributed dataset implementation for numerical data
1048#[derive(Debug)]
1049pub struct DistributedNumericalDataset {
1050    /// Raw data
1051    data: Vec<Vec<f64>>,
1052    /// Current partitions
1053    partitions: Vec<DistributedPartition<Vec<f64>>>,
1054    /// Partition assignment map
1055    assignment: HashMap<NodeId, Vec<u32>>,
1056}
1057
1058impl DistributedNumericalDataset {
1059    /// Create a new distributed numerical dataset
1060    pub fn new(data: Vec<Vec<f64>>) -> Self {
1061        Self {
1062            data,
1063            partitions: Vec::new(),
1064            assignment: HashMap::new(),
1065        }
1066    }
1067
1068    /// Build one partition per node from pre-bucketed rows.
1069    ///
1070    /// Empty buckets are skipped, and partition identifiers are assigned
1071    /// sequentially so that each identifier always matches the partition's
1072    /// position in [`Self::partitions`] (required by [`Self::get_partition`]).
1073    /// The node-to-partition assignment map is updated for every partition that
1074    /// is created.
1075    fn flush_buckets_into_partitions(&mut self, nodes: &[NodeId], buckets: Vec<Vec<Vec<f64>>>) {
1076        for (node_id, partition_data) in nodes.iter().zip(buckets) {
1077            if partition_data.is_empty() {
1078                continue;
1079            }
1080
1081            let partition_id = self.partitions.len() as u32;
1082            self.partitions.push(build_numerical_partition(
1083                partition_id,
1084                node_id,
1085                partition_data,
1086            ));
1087            self.assignment
1088                .entry(node_id.clone())
1089                .or_default()
1090                .push(partition_id);
1091        }
1092    }
1093}
1094
1095/// Mix a byte slice into a running 64-bit FNV-1a hash state.
1096#[inline]
1097fn fnv1a_mix(hash: &mut u64, bytes: &[u8]) {
1098    const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
1099    for &byte in bytes {
1100        *hash ^= u64::from(byte);
1101        *hash = hash.wrapping_mul(FNV_PRIME);
1102    }
1103}
1104
1105/// Compute a deterministic, content-derived checksum for a set of numerical
1106/// partition rows using the 64-bit FNV-1a hash over the IEEE-754 bit patterns.
1107///
1108/// The checksum is reproducible for identical data and changes whenever any
1109/// value, row length, or row count changes. The row count and per-row length
1110/// are mixed in so that different groupings of the same values yield different
1111/// checksums. Negative zero is normalized to positive zero so that
1112/// numerically-equal data produces an identical checksum.
1113fn numerical_partition_checksum(rows: &[Vec<f64>]) -> String {
1114    const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
1115
1116    let mut hash = FNV_OFFSET_BASIS;
1117    fnv1a_mix(&mut hash, &(rows.len() as u64).to_le_bytes());
1118    for row in rows {
1119        fnv1a_mix(&mut hash, &(row.len() as u64).to_le_bytes());
1120        for &value in row {
1121            let normalized = if value == 0.0 { 0.0_f64 } else { value };
1122            fnv1a_mix(&mut hash, &normalized.to_bits().to_le_bytes());
1123        }
1124    }
1125    format!("{hash:016x}")
1126}
1127
1128/// Deterministically hash a single numerical row, perturbed by `seed`, for
1129/// hash-based partitioning. Equal rows with equal seeds always hash equally,
1130/// so the resulting node assignment is reproducible.
1131fn hash_numerical_row(row: &[f64], seed: u32) -> u64 {
1132    const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
1133
1134    let mut hash = FNV_OFFSET_BASIS;
1135    fnv1a_mix(&mut hash, &u64::from(seed).to_le_bytes());
1136    for &value in row {
1137        let normalized = if value == 0.0 { 0.0_f64 } else { value };
1138        fnv1a_mix(&mut hash, &normalized.to_bits().to_le_bytes());
1139    }
1140    hash
1141}
1142
1143/// Construct a [`DistributedPartition`] from numerical rows, attaching a real,
1144/// content-derived checksum and accurate item/size metadata.
1145fn build_numerical_partition(
1146    partition_id: u32,
1147    node_id: &NodeId,
1148    partition_data: Vec<Vec<f64>>,
1149) -> DistributedPartition<Vec<f64>> {
1150    let item_count = partition_data.len() as u64;
1151    let size_bytes = item_count * std::mem::size_of::<f64>() as u64;
1152    let checksum = numerical_partition_checksum(&partition_data);
1153    let now = SystemTime::now();
1154
1155    DistributedPartition {
1156        partition_id,
1157        node_id: node_id.clone(),
1158        metadata: PartitionMetadata {
1159            item_count,
1160            size_bytes,
1161            schema: Some("numerical_array".to_string()),
1162            created_at: now,
1163            modified_at: now,
1164            checksum,
1165        },
1166        data: partition_data,
1167    }
1168}
1169
1170impl DistributedDataset for DistributedNumericalDataset {
1171    type Item = Vec<f64>;
1172    type PartitionStrategy = PartitioningStrategy;
1173
1174    fn size(&self) -> u64 {
1175        self.data.len() as u64
1176    }
1177
1178    fn partition_count(&self) -> u32 {
1179        self.partitions.len() as u32
1180    }
1181
1182    fn partition<'a>(
1183        &'a mut self,
1184        cluster: &'a dyn DistributedCluster,
1185        strategy: Self::PartitionStrategy,
1186    ) -> BoxFuture<'a, Result<Vec<DistributedPartition<Self::Item>>>> {
1187        Box::pin(async move {
1188            let nodes = cluster.active_nodes().await?;
1189            let num_nodes = nodes.len();
1190
1191            if num_nodes == 0 {
1192                return Err(SklearsError::InvalidOperation(
1193                    "No active nodes in cluster".to_string(),
1194                ));
1195            }
1196
1197            self.partitions.clear();
1198            self.assignment.clear();
1199
1200            match strategy {
1201                PartitioningStrategy::EvenSplit => {
1202                    // Contiguous, equal-sized blocks assigned by item position:
1203                    // node `i` receives the `i`-th block of the input order.
1204                    let chunk_size = self.data.len().div_ceil(num_nodes);
1205
1206                    for (i, node_id) in nodes.iter().enumerate() {
1207                        let start = i * chunk_size;
1208                        let end = std::cmp::min(start + chunk_size, self.data.len());
1209
1210                        if start < self.data.len() {
1211                            let partition_data = self.data[start..end].to_vec();
1212                            self.partitions.push(build_numerical_partition(
1213                                i as u32,
1214                                node_id,
1215                                partition_data,
1216                            ));
1217                            self.assignment
1218                                .entry(node_id.clone())
1219                                .or_default()
1220                                .push(i as u32);
1221                        }
1222                    }
1223                }
1224                PartitioningStrategy::RangeBased => {
1225                    // Order items by a representative key (the leading feature,
1226                    // or 0.0 for empty rows) using a total order so the result
1227                    // is deterministic even with NaN values, then give each node
1228                    // a contiguous slice of the value-ordered data. Partitions
1229                    // therefore correspond to disjoint value ranges rather than
1230                    // raw input positions.
1231                    let mut order: Vec<usize> = (0..self.data.len()).collect();
1232                    order.sort_by(|&a, &b| {
1233                        let key_a = self.data[a].first().copied().unwrap_or(0.0);
1234                        let key_b = self.data[b].first().copied().unwrap_or(0.0);
1235                        key_a.total_cmp(&key_b).then_with(|| a.cmp(&b))
1236                    });
1237
1238                    let chunk_size = order.len().div_ceil(num_nodes);
1239                    for (i, node_id) in nodes.iter().enumerate() {
1240                        let start = i * chunk_size;
1241                        let end = std::cmp::min(start + chunk_size, order.len());
1242
1243                        if start < order.len() {
1244                            let partition_data: Vec<Vec<f64>> = order[start..end]
1245                                .iter()
1246                                .map(|&idx| self.data[idx].clone())
1247                                .collect();
1248                            self.partitions.push(build_numerical_partition(
1249                                i as u32,
1250                                node_id,
1251                                partition_data,
1252                            ));
1253                            self.assignment
1254                                .entry(node_id.clone())
1255                                .or_default()
1256                                .push(i as u32);
1257                        }
1258                    }
1259                }
1260                PartitioningStrategy::HashBased(seed) => {
1261                    // Deterministically assign each item to node
1262                    // `hash(item, seed) % num_nodes`. Identical data and seed
1263                    // always produce the same assignment.
1264                    let mut buckets: Vec<Vec<Vec<f64>>> = vec![Vec::new(); num_nodes];
1265                    for row in &self.data {
1266                        let node_index =
1267                            (hash_numerical_row(row, seed) % num_nodes as u64) as usize;
1268                        buckets[node_index].push(row.clone());
1269                    }
1270                    self.flush_buckets_into_partitions(&nodes, buckets);
1271                }
1272                PartitioningStrategy::Random => {
1273                    // The cluster configuration carries no seed, so this
1274                    // assignment is non-deterministic across runs. Every item is
1275                    // still assigned to exactly one node, so the union of all
1276                    // partitions always reproduces the full dataset.
1277                    use scirs2_core::random::thread_rng;
1278
1279                    let mut rng = thread_rng();
1280                    let mut buckets: Vec<Vec<Vec<f64>>> = vec![Vec::new(); num_nodes];
1281                    for row in &self.data {
1282                        let node_index = rng.gen_range(0..num_nodes);
1283                        buckets[node_index].push(row.clone());
1284                    }
1285                    self.flush_buckets_into_partitions(&nodes, buckets);
1286                }
1287                PartitioningStrategy::Stratified => {
1288                    // Group rows by their class label (the trailing feature) and
1289                    // spread each stratum round-robin across the nodes, so every
1290                    // partition keeps a proportional share of every class. Strata
1291                    // are visited in a deterministic key order, and the
1292                    // round-robin cursor carries across strata to keep overall
1293                    // load balanced.
1294                    use std::collections::BTreeMap;
1295
1296                    let mut strata: BTreeMap<u64, Vec<usize>> = BTreeMap::new();
1297                    for (idx, row) in self.data.iter().enumerate() {
1298                        let label = row.last().copied().unwrap_or(0.0);
1299                        let normalized = if label == 0.0 { 0.0_f64 } else { label };
1300                        strata.entry(normalized.to_bits()).or_default().push(idx);
1301                    }
1302
1303                    let mut buckets: Vec<Vec<Vec<f64>>> = vec![Vec::new(); num_nodes];
1304                    let mut cursor = 0_usize;
1305                    for indices in strata.values() {
1306                        for &idx in indices {
1307                            buckets[cursor % num_nodes].push(self.data[idx].clone());
1308                            cursor += 1;
1309                        }
1310                    }
1311                    self.flush_buckets_into_partitions(&nodes, buckets);
1312                }
1313                PartitioningStrategy::Custom(name) => match name.as_str() {
1314                    "round_robin" | "roundrobin" | "round-robin" => {
1315                        // Round-robin: assign item `i` to node `i % num_nodes`.
1316                        let mut buckets: Vec<Vec<Vec<f64>>> = vec![Vec::new(); num_nodes];
1317                        for (idx, row) in self.data.iter().enumerate() {
1318                            buckets[idx % num_nodes].push(row.clone());
1319                        }
1320                        self.flush_buckets_into_partitions(&nodes, buckets);
1321                    }
1322                    other => {
1323                        return Err(SklearsError::InvalidOperation(format!(
1324                            "Custom partitioning strategy '{other}' is not registered"
1325                        )));
1326                    }
1327                },
1328            }
1329
1330            Ok(self.partitions.clone())
1331        })
1332    }
1333
1334    fn get_partition(
1335        &self,
1336        partition_id: u32,
1337    ) -> BoxFuture<'_, Result<DistributedPartition<Self::Item>>> {
1338        Box::pin(async move {
1339            self.partitions
1340                .get(partition_id as usize)
1341                .cloned()
1342                .ok_or_else(|| {
1343                    SklearsError::InvalidOperation(format!("Partition {} not found", partition_id))
1344                })
1345        })
1346    }
1347
1348    fn repartition<'a>(
1349        &'a mut self,
1350        cluster: &'a dyn DistributedCluster,
1351        new_strategy: Self::PartitionStrategy,
1352    ) -> BoxFuture<'a, Result<()>> {
1353        Box::pin(async move {
1354            // Collect all data back first
1355            let collected_data = self.collect(cluster).await?;
1356            self.data = collected_data;
1357
1358            // Repartition with new strategy
1359            self.partition(cluster, new_strategy).await?;
1360
1361            Ok(())
1362        })
1363    }
1364
1365    fn collect(&self, _cluster: &dyn DistributedCluster) -> BoxFuture<'_, Result<Vec<Self::Item>>> {
1366        Box::pin(async move {
1367            let mut collected = Vec::new();
1368            for partition in &self.partitions {
1369                collected.extend(partition.data.clone());
1370            }
1371            Ok(collected)
1372        })
1373    }
1374
1375    fn partition_assignment(&self) -> HashMap<NodeId, Vec<u32>> {
1376        self.assignment.clone()
1377    }
1378}
1379
1380#[allow(non_snake_case)]
1381#[cfg(test)]
1382mod tests {
1383    use super::*;
1384
1385    #[test]
1386    fn test_node_id_creation() {
1387        let node_id = NodeId::new("worker-01");
1388        assert_eq!(node_id.as_str(), "worker-01");
1389        assert_eq!(node_id.to_string(), "worker-01");
1390    }
1391
1392    #[test]
1393    fn test_message_priority_ordering() {
1394        assert!(MessagePriority::Critical > MessagePriority::High);
1395        assert!(MessagePriority::High > MessagePriority::Normal);
1396        assert!(MessagePriority::Normal > MessagePriority::Low);
1397    }
1398
1399    #[test]
1400    fn test_cluster_configuration_default() {
1401        let config = ClusterConfiguration::default();
1402        assert_eq!(config.max_nodes, 64);
1403        assert_eq!(config.load_balancing, LoadBalancingStrategy::ResourceBased);
1404        assert_eq!(
1405            config.fault_tolerance,
1406            FaultToleranceMode::CheckpointRecovery
1407        );
1408    }
1409
1410    #[test]
1411    fn test_distributed_linear_regression_creation() {
1412        let model = DistributedLinearRegression::new();
1413        assert!(model.parameters.is_none());
1414        assert_eq!(model.progress.epoch, 0);
1415    }
1416
1417    #[test]
1418    fn test_distributed_dataset_size() {
1419        let data = vec![vec![1.0, 2.0], vec![3.0, 4.0], vec![5.0, 6.0]];
1420        let dataset = DistributedNumericalDataset::new(data);
1421        assert_eq!(dataset.size(), 3);
1422        assert_eq!(dataset.partition_count(), 0); // No partitions initially
1423    }
1424
1425    #[test]
1426    fn test_message_type_serialization() {
1427        let msg_type = MessageType::ParameterSync;
1428        let serialized = serde_json::to_string(&msg_type).unwrap_or_default();
1429        let deserialized: MessageType =
1430            serde_json::from_str(&serialized).expect("valid JSON operation");
1431        assert_eq!(msg_type, deserialized);
1432    }
1433
1434    #[test]
1435    fn test_partitioning_strategy_variants() {
1436        let strategies = vec![
1437            PartitioningStrategy::EvenSplit,
1438            PartitioningStrategy::HashBased(4),
1439            PartitioningStrategy::RangeBased,
1440            PartitioningStrategy::Random,
1441            PartitioningStrategy::Stratified,
1442            PartitioningStrategy::Custom("custom_strategy".to_string()),
1443        ];
1444
1445        for strategy in strategies {
1446            let serialized = serde_json::to_string(&strategy).unwrap_or_default();
1447            let _deserialized: PartitioningStrategy =
1448                serde_json::from_str(&serialized).expect("valid JSON operation");
1449        }
1450    }
1451
1452    #[test]
1453    fn test_distributed_training_config() {
1454        let config = DistributedTrainingConfig::default();
1455        assert_eq!(config.learning_rate, 0.01);
1456        assert_eq!(config.epochs, 100);
1457        assert_eq!(config.batch_size, 32);
1458    }
1459
1460    #[cfg(feature = "async_support")]
1461    #[tokio::test]
1462    async fn test_default_cluster_operations() {
1463        let coordinator = NodeId::new("coordinator");
1464        let config = ClusterConfiguration::default();
1465        let cluster = DefaultDistributedCluster::new(coordinator.clone(), config);
1466
1467        assert_eq!(cluster.coordinator(), &coordinator);
1468
1469        let nodes = cluster.active_nodes().await.expect("expected valid value");
1470        assert!(nodes.is_empty()); // No nodes initially
1471
1472        let health = cluster
1473            .cluster_health()
1474            .await
1475            .expect("expected valid value");
1476        assert_eq!(health.overall_health, 1.0);
1477    }
1478
1479    #[test]
1480    fn test_numerical_partition_checksum_is_deterministic() {
1481        let data = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
1482        let first = numerical_partition_checksum(&data);
1483        let second = numerical_partition_checksum(&data);
1484        assert_eq!(
1485            first, second,
1486            "identical data must produce identical checksums"
1487        );
1488    }
1489
1490    #[test]
1491    fn test_numerical_partition_checksum_changes_with_data() {
1492        let data_a = vec![vec![1.0, 2.0, 3.0]];
1493        let data_b = vec![vec![1.0, 2.0, 4.0]];
1494        assert_ne!(
1495            numerical_partition_checksum(&data_a),
1496            numerical_partition_checksum(&data_b),
1497            "different data must produce different checksums"
1498        );
1499    }
1500
1501    #[test]
1502    fn test_numerical_partition_checksum_respects_row_structure() {
1503        // The same values grouped into different rows must hash differently.
1504        let grouped_a = vec![vec![1.0, 2.0], vec![3.0]];
1505        let grouped_b = vec![vec![1.0], vec![2.0, 3.0]];
1506        assert_ne!(
1507            numerical_partition_checksum(&grouped_a),
1508            numerical_partition_checksum(&grouped_b)
1509        );
1510    }
1511
1512    #[test]
1513    fn test_numerical_partition_checksum_is_not_index_placeholder() {
1514        // Guard against regressing to the fabricated `checksum_{index}` form.
1515        let checksum = numerical_partition_checksum(&[vec![1.0, 2.0]]);
1516        assert!(!checksum.starts_with("checksum_"));
1517        assert_eq!(checksum.len(), 16);
1518    }
1519
1520    #[test]
1521    fn test_hash_numerical_row_determinism_and_seed_sensitivity() {
1522        let row = vec![1.5, -2.25, 3.0];
1523        assert_eq!(hash_numerical_row(&row, 7), hash_numerical_row(&row, 7));
1524        assert_ne!(hash_numerical_row(&row, 7), hash_numerical_row(&row, 8));
1525    }
1526
1527    #[cfg(feature = "async_support")]
1528    struct TestCluster {
1529        coordinator: NodeId,
1530        configuration: ClusterConfiguration,
1531        nodes: Vec<NodeId>,
1532    }
1533
1534    #[cfg(feature = "async_support")]
1535    impl TestCluster {
1536        fn with_nodes(count: usize) -> Self {
1537            let nodes = (0..count)
1538                .map(|i| NodeId::new(format!("node-{i}")))
1539                .collect();
1540            Self {
1541                coordinator: NodeId::new("coordinator"),
1542                configuration: ClusterConfiguration::default(),
1543                nodes,
1544            }
1545        }
1546    }
1547
1548    #[cfg(feature = "async_support")]
1549    impl DistributedCluster for TestCluster {
1550        fn active_nodes(&self) -> BoxFuture<'_, Result<Vec<NodeId>>> {
1551            let nodes = self.nodes.clone();
1552            Box::pin(async move { Ok(nodes) })
1553        }
1554
1555        fn coordinator(&self) -> &NodeId {
1556            &self.coordinator
1557        }
1558
1559        fn configuration(&self) -> &ClusterConfiguration {
1560            &self.configuration
1561        }
1562
1563        fn add_node(&mut self, node: NodeId) -> BoxFuture<'_, Result<()>> {
1564            self.nodes.push(node);
1565            Box::pin(async move { Ok(()) })
1566        }
1567
1568        fn remove_node(&mut self, node: NodeId) -> BoxFuture<'_, Result<()>> {
1569            self.nodes.retain(|existing| existing != &node);
1570            Box::pin(async move { Ok(()) })
1571        }
1572
1573        fn rebalance_load(&mut self) -> BoxFuture<'_, Result<()>> {
1574            Box::pin(async move { Ok(()) })
1575        }
1576
1577        fn cluster_health(&self) -> BoxFuture<'_, Result<ClusterHealth>> {
1578            Box::pin(async move {
1579                Err(SklearsError::InvalidOperation(
1580                    "cluster_health is unused in partition tests".to_string(),
1581                ))
1582            })
1583        }
1584
1585        fn create_checkpoint(&self) -> BoxFuture<'_, Result<ClusterCheckpoint>> {
1586            Box::pin(async move {
1587                Err(SklearsError::InvalidOperation(
1588                    "create_checkpoint is unused in partition tests".to_string(),
1589                ))
1590            })
1591        }
1592
1593        fn restore_checkpoint(
1594            &mut self,
1595            _checkpoint: ClusterCheckpoint,
1596        ) -> BoxFuture<'_, Result<()>> {
1597            Box::pin(async move { Ok(()) })
1598        }
1599    }
1600
1601    #[cfg(feature = "async_support")]
1602    fn parse_node_index(node_id: &NodeId) -> usize {
1603        node_id
1604            .as_str()
1605            .strip_prefix("node-")
1606            .and_then(|suffix| suffix.parse().ok())
1607            .expect("test node ids follow the node-<index> convention")
1608    }
1609
1610    #[cfg(feature = "async_support")]
1611    #[tokio::test]
1612    async fn test_partition_without_nodes_errors() {
1613        let mut dataset = DistributedNumericalDataset::new(vec![vec![1.0]]);
1614        let cluster = TestCluster::with_nodes(0);
1615        let result = dataset
1616            .partition(&cluster, PartitioningStrategy::EvenSplit)
1617            .await;
1618        assert!(result.is_err(), "partitioning with no nodes must error");
1619    }
1620
1621    #[cfg(feature = "async_support")]
1622    #[tokio::test]
1623    async fn test_even_split_covers_all_items() {
1624        let data: Vec<Vec<f64>> = (0..10).map(|i| vec![i as f64, (i * 2) as f64]).collect();
1625        let mut dataset = DistributedNumericalDataset::new(data.clone());
1626        let cluster = TestCluster::with_nodes(3);
1627
1628        let partitions = dataset
1629            .partition(&cluster, PartitioningStrategy::EvenSplit)
1630            .await
1631            .expect("even split must succeed");
1632
1633        let mut collected: Vec<Vec<f64>> = partitions.iter().flat_map(|p| p.data.clone()).collect();
1634        collected.sort_by(|a, b| a[0].total_cmp(&b[0]));
1635        assert_eq!(collected, data, "union of partitions must cover all items");
1636    }
1637
1638    #[cfg(feature = "async_support")]
1639    #[tokio::test]
1640    async fn test_round_robin_custom_strategy_assignment() {
1641        let data: Vec<Vec<f64>> = (0..9).map(|i| vec![i as f64]).collect();
1642        let mut dataset = DistributedNumericalDataset::new(data);
1643        let cluster = TestCluster::with_nodes(3);
1644
1645        let partitions = dataset
1646            .partition(
1647                &cluster,
1648                PartitioningStrategy::Custom("round_robin".to_string()),
1649            )
1650            .await
1651            .expect("round-robin custom strategy must succeed");
1652
1653        assert_eq!(partitions.len(), 3);
1654        // Item `i` must land on node `i % num_nodes`: every value in a node's
1655        // partition shares the same `value % num_nodes` class.
1656        for partition in &partitions {
1657            let node_index = parse_node_index(&partition.node_id);
1658            for row in &partition.data {
1659                assert_eq!((row[0] as usize) % 3, node_index);
1660            }
1661            assert_eq!(partition.data.len(), 3);
1662        }
1663    }
1664
1665    #[cfg(feature = "async_support")]
1666    #[tokio::test]
1667    async fn test_hash_based_partitioning_is_deterministic() {
1668        let data: Vec<Vec<f64>> = (0..20).map(|i| vec![i as f64, (i % 4) as f64]).collect();
1669        let cluster = TestCluster::with_nodes(4);
1670
1671        let mut first_dataset = DistributedNumericalDataset::new(data.clone());
1672        let mut second_dataset = DistributedNumericalDataset::new(data.clone());
1673
1674        let first = first_dataset
1675            .partition(&cluster, PartitioningStrategy::HashBased(13))
1676            .await
1677            .expect("hash partitioning must succeed");
1678        let second = second_dataset
1679            .partition(&cluster, PartitioningStrategy::HashBased(13))
1680            .await
1681            .expect("hash partitioning must succeed");
1682
1683        let summarize = |partitions: &[DistributedPartition<Vec<f64>>]| {
1684            partitions
1685                .iter()
1686                .map(|p| (p.partition_id, p.data.clone()))
1687                .collect::<Vec<_>>()
1688        };
1689        assert_eq!(
1690            summarize(&first),
1691            summarize(&second),
1692            "hash partitioning must be deterministic for identical data and seed"
1693        );
1694
1695        // Every item must reside on node `hash(item, seed) % num_nodes`.
1696        for partition in &first {
1697            let node_index = parse_node_index(&partition.node_id);
1698            for row in &partition.data {
1699                assert_eq!((hash_numerical_row(row, 13) % 4) as usize, node_index);
1700            }
1701        }
1702
1703        // Union of partitions reproduces the entire dataset.
1704        let mut all: Vec<Vec<f64>> = first.iter().flat_map(|p| p.data.clone()).collect();
1705        all.sort_by(|a, b| a[0].total_cmp(&b[0]));
1706        assert_eq!(all, data);
1707    }
1708
1709    #[cfg(feature = "async_support")]
1710    #[tokio::test]
1711    async fn test_range_based_partitioning_orders_by_value() {
1712        let data: Vec<Vec<f64>> = vec![
1713            vec![9.0],
1714            vec![1.0],
1715            vec![7.0],
1716            vec![3.0],
1717            vec![5.0],
1718            vec![2.0],
1719        ];
1720        let mut dataset = DistributedNumericalDataset::new(data.clone());
1721        let cluster = TestCluster::with_nodes(3);
1722
1723        let partitions = dataset
1724            .partition(&cluster, PartitioningStrategy::RangeBased)
1725            .await
1726            .expect("range partitioning must succeed");
1727
1728        // Flattening in partition order must yield globally value-ordered keys.
1729        let ordered_keys: Vec<f64> = partitions
1730            .iter()
1731            .flat_map(|p| p.data.iter().map(|row| row[0]))
1732            .collect();
1733        let mut expected: Vec<f64> = data.iter().map(|row| row[0]).collect();
1734        expected.sort_by(|a, b| a.total_cmp(b));
1735        assert_eq!(
1736            ordered_keys, expected,
1737            "range partitions must be value-ordered"
1738        );
1739
1740        // 6 items across 3 nodes => 2 contiguous items each.
1741        assert_eq!(partitions.len(), 3);
1742        for partition in &partitions {
1743            assert_eq!(partition.data.len(), 2);
1744        }
1745    }
1746
1747    #[cfg(feature = "async_support")]
1748    #[tokio::test]
1749    async fn test_stratified_partitioning_balances_classes() {
1750        // The trailing column is the class label; 3 classes with 6 items each.
1751        let mut data: Vec<Vec<f64>> = Vec::new();
1752        for class in 0..3 {
1753            for k in 0..6 {
1754                data.push(vec![k as f64, class as f64]);
1755            }
1756        }
1757        let mut dataset = DistributedNumericalDataset::new(data.clone());
1758        let cluster = TestCluster::with_nodes(3);
1759
1760        let partitions = dataset
1761            .partition(&cluster, PartitioningStrategy::Stratified)
1762            .await
1763            .expect("stratified partitioning must succeed");
1764
1765        let total: usize = partitions.iter().map(|p| p.data.len()).sum();
1766        assert_eq!(total, data.len(), "stratified must not drop items");
1767
1768        // 18 items / 3 nodes => 6 per node; 3 classes => 2 of each class per node.
1769        for partition in &partitions {
1770            let mut class_counts: HashMap<i64, usize> = HashMap::new();
1771            for row in &partition.data {
1772                *class_counts.entry(row[1] as i64).or_insert(0) += 1;
1773            }
1774            assert_eq!(
1775                class_counts.len(),
1776                3,
1777                "each partition must hold all classes"
1778            );
1779            for count in class_counts.values() {
1780                assert_eq!(*count, 2, "each class must be evenly represented");
1781            }
1782        }
1783    }
1784
1785    #[cfg(feature = "async_support")]
1786    #[tokio::test]
1787    async fn test_random_partitioning_covers_all_items() {
1788        let data: Vec<Vec<f64>> = (0..30).map(|i| vec![i as f64]).collect();
1789        let mut dataset = DistributedNumericalDataset::new(data);
1790        let cluster = TestCluster::with_nodes(4);
1791
1792        let partitions = dataset
1793            .partition(&cluster, PartitioningStrategy::Random)
1794            .await
1795            .expect("random partitioning must succeed");
1796
1797        let mut all: Vec<f64> = partitions
1798            .iter()
1799            .flat_map(|p| p.data.iter().map(|row| row[0]))
1800            .collect();
1801        all.sort_by(|a, b| a.total_cmp(b));
1802        let expected: Vec<f64> = (0..30).map(|i| i as f64).collect();
1803        assert_eq!(
1804            all, expected,
1805            "random partitioning must not lose or duplicate items"
1806        );
1807    }
1808
1809    #[cfg(feature = "async_support")]
1810    #[tokio::test]
1811    async fn test_unknown_custom_strategy_errors() {
1812        let mut dataset = DistributedNumericalDataset::new(vec![vec![1.0], vec![2.0]]);
1813        let cluster = TestCluster::with_nodes(2);
1814
1815        let result = dataset
1816            .partition(
1817                &cluster,
1818                PartitioningStrategy::Custom("totally_unknown".to_string()),
1819            )
1820            .await;
1821        assert!(
1822            result.is_err(),
1823            "unknown custom strategy must return an honest error"
1824        );
1825    }
1826
1827    #[cfg(feature = "async_support")]
1828    #[tokio::test]
1829    async fn test_partition_checksums_reflect_content() {
1830        let cluster = TestCluster::with_nodes(1);
1831
1832        let mut dataset_a = DistributedNumericalDataset::new(vec![vec![1.0, 2.0], vec![3.0, 4.0]]);
1833        let mut dataset_b = DistributedNumericalDataset::new(vec![vec![1.0, 2.0], vec![3.0, 5.0]]);
1834        let mut dataset_a_again =
1835            DistributedNumericalDataset::new(vec![vec![1.0, 2.0], vec![3.0, 4.0]]);
1836
1837        let partitions_a = dataset_a
1838            .partition(&cluster, PartitioningStrategy::EvenSplit)
1839            .await
1840            .expect("partition a");
1841        let partitions_b = dataset_b
1842            .partition(&cluster, PartitioningStrategy::EvenSplit)
1843            .await
1844            .expect("partition b");
1845        let partitions_a_again = dataset_a_again
1846            .partition(&cluster, PartitioningStrategy::EvenSplit)
1847            .await
1848            .expect("partition a again");
1849
1850        assert_eq!(partitions_a.len(), 1);
1851        assert_eq!(partitions_b.len(), 1);
1852        assert_ne!(
1853            partitions_a[0].metadata.checksum, partitions_b[0].metadata.checksum,
1854            "different data must yield different checksums"
1855        );
1856        assert_eq!(
1857            partitions_a[0].metadata.checksum, partitions_a_again[0].metadata.checksum,
1858            "identical data must yield identical checksums"
1859        );
1860        assert!(!partitions_a[0].metadata.checksum.starts_with("checksum_"));
1861    }
1862}