Skip to main content

oxirs_stream/
scalability.rs

1//! # Scalability Features for Stream Processing
2//!
3//! This module provides comprehensive scalability capabilities including
4//! horizontal and vertical scaling, adaptive buffering, and dynamic resource management.
5//!
6//! ## Features
7//!
8//! - **Horizontal Scaling**: Dynamic partition management and load balancing
9//! - **Vertical Scaling**: Adaptive resource allocation
10//! - **Adaptive Buffering**: Smart buffer sizing based on load
11//! - **Dynamic Partitioning**: Automatic partition assignment and rebalancing
12//! - **Resource Optimization**: CPU, memory, and network optimization
13//! - **Auto-scaling**: Automatic scaling based on metrics
14//!
15//! ## Use Cases
16//!
17//! - **High Throughput**: Handle millions of events per second
18//! - **Burst Handling**: Adapt to traffic spikes
19//! - **Cost Optimization**: Scale down during low traffic
20//! - **Fault Tolerance**: Redistribute load on failures
21
22use anyhow::{anyhow, Result};
23use chrono::{DateTime, Utc};
24use dashmap::DashMap;
25use parking_lot::RwLock;
26use serde::{Deserialize, Serialize};
27use std::collections::{HashMap, VecDeque};
28use std::sync::Arc;
29use std::time::{Duration, Instant};
30use tokio::sync::mpsc;
31use tracing::{debug, error, info, warn};
32use uuid::Uuid;
33
34// Use SciRS2 for scientific computing and statistics
35
36/// Simple moving average calculator
37struct MovingAverage {
38    window_size: usize,
39    values: VecDeque<f64>,
40    sum: f64,
41}
42
43impl MovingAverage {
44    fn new(window_size: usize) -> Self {
45        Self {
46            window_size,
47            values: VecDeque::with_capacity(window_size),
48            sum: 0.0,
49        }
50    }
51
52    fn add(&mut self, value: f64) {
53        if self.values.len() >= self.window_size {
54            if let Some(old) = self.values.pop_front() {
55                self.sum -= old;
56            }
57        }
58        self.values.push_back(value);
59        self.sum += value;
60    }
61
62    fn mean(&self) -> f64 {
63        if self.values.is_empty() {
64            0.0
65        } else {
66            self.sum / self.values.len() as f64
67        }
68    }
69}
70
71/// Scaling mode
72#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
73pub enum ScalingMode {
74    /// Manual scaling - no automatic scaling
75    Manual,
76    /// Horizontal scaling - add/remove partitions
77    Horizontal,
78    /// Vertical scaling - adjust resources
79    Vertical,
80    /// Both horizontal and vertical
81    Hybrid,
82}
83
84/// Scaling direction
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub enum ScalingDirection {
87    /// Scale up
88    ScaleUp { amount: usize },
89    /// Scale down
90    ScaleDown { amount: usize },
91    /// No scaling needed
92    NoChange,
93}
94
95/// Partition strategy
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub enum PartitionStrategy {
98    /// Round-robin partitioning
99    RoundRobin,
100    /// Hash-based partitioning
101    Hash { key_field: String },
102    /// Range-based partitioning
103    Range { ranges: Vec<(i64, i64)> },
104    /// Consistent hashing
105    ConsistentHash { virtual_nodes: usize },
106    /// Custom partitioning
107    Custom { strategy_name: String },
108}
109
110/// Load balancing strategy
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub enum LoadBalancingStrategy {
113    /// Round-robin
114    RoundRobin,
115    /// Least connections
116    LeastConnections,
117    /// Least loaded
118    LeastLoaded,
119    /// Weighted distribution
120    Weighted { weights: HashMap<String, f64> },
121    /// Consistent hashing
122    ConsistentHash,
123}
124
125/// Resource limits
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct ResourceLimits {
128    /// Maximum CPU cores
129    pub max_cpu_cores: usize,
130    /// Maximum memory (bytes)
131    pub max_memory_bytes: u64,
132    /// Maximum network bandwidth (bytes/sec)
133    pub max_network_bandwidth: u64,
134    /// Maximum partitions
135    pub max_partitions: usize,
136    /// Minimum partitions
137    pub min_partitions: usize,
138}
139
140/// Scaling configuration
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct ScalingConfig {
143    /// Scaling mode
144    pub mode: ScalingMode,
145    /// Partition strategy
146    pub partition_strategy: PartitionStrategy,
147    /// Load balancing strategy
148    pub load_balancing: LoadBalancingStrategy,
149    /// Resource limits
150    pub resource_limits: ResourceLimits,
151    /// Scale-up threshold (0.0 to 1.0)
152    pub scale_up_threshold: f64,
153    /// Scale-down threshold (0.0 to 1.0)
154    pub scale_down_threshold: f64,
155    /// Cooldown period between scaling operations
156    pub cooldown_period: Duration,
157    /// Enable adaptive buffering
158    pub enable_adaptive_buffering: bool,
159    /// Initial buffer size
160    pub initial_buffer_size: usize,
161    /// Maximum buffer size
162    pub max_buffer_size: usize,
163    /// Minimum buffer size
164    pub min_buffer_size: usize,
165}
166
167impl Default for ScalingConfig {
168    fn default() -> Self {
169        Self {
170            mode: ScalingMode::Hybrid,
171            partition_strategy: PartitionStrategy::RoundRobin,
172            load_balancing: LoadBalancingStrategy::LeastLoaded,
173            resource_limits: ResourceLimits {
174                max_cpu_cores: std::thread::available_parallelism()
175                    .map(|n| n.get())
176                    .unwrap_or(1),
177                max_memory_bytes: 8 * 1024 * 1024 * 1024, // 8GB
178                max_network_bandwidth: 1_000_000_000,     // 1 Gbps
179                max_partitions: 100,
180                min_partitions: 1,
181            },
182            scale_up_threshold: 0.8,
183            scale_down_threshold: 0.3,
184            cooldown_period: Duration::from_secs(60),
185            enable_adaptive_buffering: true,
186            initial_buffer_size: 10000,
187            max_buffer_size: 1000000,
188            min_buffer_size: 1000,
189        }
190    }
191}
192
193/// Partition metadata
194#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct Partition {
196    /// Partition ID
197    pub partition_id: String,
198    /// Partition number
199    pub partition_number: usize,
200    /// Owner node
201    pub owner_node: Option<String>,
202    /// Replica nodes
203    pub replica_nodes: Vec<String>,
204    /// Current load (0.0 to 1.0)
205    pub load: f64,
206    /// Number of events
207    pub event_count: u64,
208    /// Created at
209    pub created_at: DateTime<Utc>,
210    /// Last updated
211    pub last_updated: DateTime<Utc>,
212}
213
214/// Node metadata
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct Node {
217    /// Node ID
218    pub node_id: String,
219    /// Node address
220    pub address: String,
221    /// Assigned partitions
222    pub partitions: Vec<usize>,
223    /// Resource usage
224    pub resource_usage: ResourceUsage,
225    /// Health status
226    pub health: NodeHealth,
227    /// Last heartbeat
228    pub last_heartbeat: DateTime<Utc>,
229}
230
231/// Resource usage
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct ResourceUsage {
234    /// CPU usage (0.0 to 1.0)
235    pub cpu_usage: f64,
236    /// Memory usage (bytes)
237    pub memory_usage: u64,
238    /// Network usage (bytes/sec)
239    pub network_usage: u64,
240    /// Events per second
241    pub events_per_second: f64,
242}
243
244/// Node health status
245#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
246pub enum NodeHealth {
247    Healthy,
248    Degraded,
249    Unhealthy,
250    Offline,
251}
252
253/// Adaptive buffer
254pub struct AdaptiveBuffer<T> {
255    /// Buffer configuration
256    config: ScalingConfig,
257    /// Current buffer
258    buffer: Arc<RwLock<VecDeque<T>>>,
259    /// Current buffer size limit
260    current_size: Arc<RwLock<usize>>,
261    /// Load statistics (using SciRS2)
262    load_history: Arc<RwLock<VecDeque<f64>>>,
263    /// Moving average calculator
264    moving_avg: Arc<RwLock<MovingAverage>>,
265    /// Last resize time
266    last_resize: Arc<RwLock<Instant>>,
267}
268
269impl<T> AdaptiveBuffer<T> {
270    /// Create a new adaptive buffer
271    pub fn new(config: ScalingConfig) -> Self {
272        Self {
273            current_size: Arc::new(RwLock::new(config.initial_buffer_size)),
274            config,
275            buffer: Arc::new(RwLock::new(VecDeque::new())),
276            load_history: Arc::new(RwLock::new(VecDeque::with_capacity(100))),
277            moving_avg: Arc::new(RwLock::new(MovingAverage::new(10))),
278            last_resize: Arc::new(RwLock::new(Instant::now())),
279        }
280    }
281
282    /// Push an item to the buffer
283    pub fn push(&self, item: T) -> Result<()> {
284        let mut buffer = self.buffer.write();
285        let current_size = *self.current_size.read();
286
287        if buffer.len() >= current_size {
288            // Buffer is full - try to resize or reject
289            self.try_resize()?;
290
291            let new_size = *self.current_size.read();
292            if buffer.len() >= new_size {
293                return Err(anyhow!("Buffer full: {}/{}", buffer.len(), new_size));
294            }
295        }
296
297        buffer.push_back(item);
298        self.update_load_metrics(buffer.len(), current_size);
299        Ok(())
300    }
301
302    /// Pop an item from the buffer
303    pub fn pop(&self) -> Option<T> {
304        let mut buffer = self.buffer.write();
305        let item = buffer.pop_front();
306
307        let current_size = *self.current_size.read();
308        self.update_load_metrics(buffer.len(), current_size);
309        item
310    }
311
312    /// Get current buffer utilization
313    pub fn utilization(&self) -> f64 {
314        let buffer = self.buffer.read();
315        let current_size = *self.current_size.read();
316        buffer.len() as f64 / current_size as f64
317    }
318
319    /// Update load metrics
320    fn update_load_metrics(&self, buffer_len: usize, current_size: usize) {
321        let load = buffer_len as f64 / current_size as f64;
322
323        let mut history = self.load_history.write();
324        history.push_back(load);
325        if history.len() > 100 {
326            history.pop_front();
327        }
328
329        let mut moving_avg = self.moving_avg.write();
330        moving_avg.add(load);
331    }
332
333    /// Try to resize the buffer based on load
334    fn try_resize(&self) -> Result<()> {
335        if !self.config.enable_adaptive_buffering {
336            return Ok(());
337        }
338
339        let last_resize = *self.last_resize.read();
340        if last_resize.elapsed() < Duration::from_secs(10) {
341            // Cooldown period
342            return Ok(());
343        }
344
345        let moving_avg = self.moving_avg.read();
346        let avg_load = moving_avg.mean();
347
348        let mut current_size = self.current_size.write();
349
350        if avg_load > self.config.scale_up_threshold {
351            // Scale up
352            let new_size = (*current_size * 2).min(self.config.max_buffer_size);
353            if new_size > *current_size {
354                *current_size = new_size;
355                *self.last_resize.write() = Instant::now();
356                info!("Scaled up buffer to {}", new_size);
357            }
358        } else if avg_load < self.config.scale_down_threshold {
359            // Scale down
360            let new_size = (*current_size / 2).max(self.config.min_buffer_size);
361            if new_size < *current_size {
362                *current_size = new_size;
363                *self.last_resize.write() = Instant::now();
364                info!("Scaled down buffer to {}", new_size);
365            }
366        }
367
368        Ok(())
369    }
370
371    /// Get current buffer size
372    pub fn size(&self) -> usize {
373        *self.current_size.read()
374    }
375
376    /// Get current buffer length
377    pub fn len(&self) -> usize {
378        self.buffer.read().len()
379    }
380
381    /// Check if buffer is empty
382    pub fn is_empty(&self) -> bool {
383        self.buffer.read().is_empty()
384    }
385}
386
387/// Partition manager for horizontal scaling
388pub struct PartitionManager {
389    /// Configuration
390    config: ScalingConfig,
391    /// Partitions
392    partitions: Arc<DashMap<usize, Partition>>,
393    /// Nodes
394    nodes: Arc<DashMap<String, Node>>,
395    /// Partition assignments
396    assignments: Arc<DashMap<usize, String>>,
397    /// Last scaling operation
398    last_scaling: Arc<RwLock<Instant>>,
399    /// Counter for round-robin
400    counter: Arc<RwLock<usize>>,
401}
402
403impl PartitionManager {
404    /// Create a new partition manager
405    pub fn new(config: ScalingConfig) -> Self {
406        let manager = Self {
407            config: config.clone(),
408            partitions: Arc::new(DashMap::new()),
409            nodes: Arc::new(DashMap::new()),
410            assignments: Arc::new(DashMap::new()),
411            last_scaling: Arc::new(RwLock::new(Instant::now())),
412            counter: Arc::new(RwLock::new(0)),
413        };
414
415        // Initialize with minimum partitions
416        for i in 0..config.resource_limits.min_partitions {
417            manager.create_partition(i);
418        }
419
420        manager
421    }
422
423    /// Create a new partition
424    fn create_partition(&self, partition_number: usize) {
425        let partition = Partition {
426            partition_id: Uuid::new_v4().to_string(),
427            partition_number,
428            owner_node: None,
429            replica_nodes: Vec::new(),
430            load: 0.0,
431            event_count: 0,
432            created_at: Utc::now(),
433            last_updated: Utc::now(),
434        };
435
436        self.partitions.insert(partition_number, partition);
437        info!("Created partition {}", partition_number);
438    }
439
440    /// Add a node to the cluster
441    pub fn add_node(&self, node: Node) -> Result<()> {
442        let node_id = node.node_id.clone();
443        self.nodes.insert(node_id.clone(), node);
444
445        // Rebalance partitions
446        self.rebalance_partitions()?;
447
448        info!("Added node {}", node_id);
449        Ok(())
450    }
451
452    /// Remove a node from the cluster
453    pub fn remove_node(&self, node_id: &str) -> Result<()> {
454        self.nodes.remove(node_id);
455
456        // Reassign partitions from this node
457        self.rebalance_partitions()?;
458
459        info!("Removed node {}", node_id);
460        Ok(())
461    }
462
463    /// Rebalance partitions across nodes
464    fn rebalance_partitions(&self) -> Result<()> {
465        let nodes: Vec<_> = self.nodes.iter().map(|e| e.key().clone()).collect();
466
467        if nodes.is_empty() {
468            warn!("No nodes available for partition assignment");
469            return Ok(());
470        }
471
472        let partitions: Vec<_> = self.partitions.iter().map(|e| *e.key()).collect();
473
474        // Simple round-robin assignment
475        for (idx, partition_num) in partitions.iter().enumerate() {
476            let node_id = &nodes[idx % nodes.len()];
477            self.assignments.insert(*partition_num, node_id.clone());
478
479            // Update partition owner
480            if let Some(mut partition) = self.partitions.get_mut(partition_num) {
481                partition.owner_node = Some(node_id.clone());
482                partition.last_updated = Utc::now();
483            }
484
485            // Update node partitions
486            if let Some(mut node) = self.nodes.get_mut(node_id) {
487                if !node.partitions.contains(partition_num) {
488                    node.partitions.push(*partition_num);
489                }
490            }
491        }
492
493        debug!(
494            "Rebalanced {} partitions across {} nodes",
495            partitions.len(),
496            nodes.len()
497        );
498        Ok(())
499    }
500
501    /// Evaluate scaling needs
502    pub fn evaluate_scaling(&self) -> ScalingDirection {
503        if !matches!(
504            self.config.mode,
505            ScalingMode::Horizontal | ScalingMode::Hybrid
506        ) {
507            return ScalingDirection::NoChange;
508        }
509
510        // Check cooldown
511        if self.last_scaling.read().elapsed() < self.config.cooldown_period {
512            return ScalingDirection::NoChange;
513        }
514
515        // Calculate average load across partitions
516        let partitions: Vec<_> = self.partitions.iter().map(|e| e.clone()).collect();
517
518        if partitions.is_empty() {
519            return ScalingDirection::NoChange;
520        }
521
522        let avg_load = partitions.iter().map(|p| p.load).sum::<f64>() / partitions.len() as f64;
523
524        if avg_load > self.config.scale_up_threshold
525            && partitions.len() < self.config.resource_limits.max_partitions
526        {
527            // Scale up
528            let amount = ((partitions.len() as f64 * 0.5).ceil() as usize)
529                .min(self.config.resource_limits.max_partitions - partitions.len())
530                .max(1);
531            ScalingDirection::ScaleUp { amount }
532        } else if avg_load < self.config.scale_down_threshold
533            && partitions.len() > self.config.resource_limits.min_partitions
534        {
535            // Scale down
536            let amount = ((partitions.len() as f64 * 0.25).ceil() as usize)
537                .min(partitions.len() - self.config.resource_limits.min_partitions)
538                .max(1);
539            ScalingDirection::ScaleDown { amount }
540        } else {
541            ScalingDirection::NoChange
542        }
543    }
544
545    /// Apply scaling decision
546    pub fn apply_scaling(&self, direction: &ScalingDirection) -> Result<()> {
547        match direction {
548            ScalingDirection::ScaleUp { amount } => {
549                let current_max = self.partitions.iter().map(|e| *e.key()).max().unwrap_or(0);
550
551                for i in 1..=*amount {
552                    let partition_num = current_max + i;
553                    if partition_num < self.config.resource_limits.max_partitions {
554                        self.create_partition(partition_num);
555                    }
556                }
557
558                self.rebalance_partitions()?;
559                *self.last_scaling.write() = Instant::now();
560
561                info!("Scaled up by {} partitions", amount);
562            }
563            ScalingDirection::ScaleDown { amount } => {
564                let partition_nums: Vec<_> = self.partitions.iter().map(|e| *e.key()).collect();
565
566                // Remove the highest numbered partitions
567                let mut removed = 0;
568                for partition_num in partition_nums.iter().rev() {
569                    if removed >= *amount {
570                        break;
571                    }
572                    if partition_nums.len() - removed > self.config.resource_limits.min_partitions {
573                        self.partitions.remove(partition_num);
574                        self.assignments.remove(partition_num);
575                        removed += 1;
576                    }
577                }
578
579                self.rebalance_partitions()?;
580                *self.last_scaling.write() = Instant::now();
581
582                info!("Scaled down by {} partitions", removed);
583            }
584            ScalingDirection::NoChange => {}
585        }
586
587        Ok(())
588    }
589
590    /// Get partition for a key
591    pub fn get_partition_for_key(&self, key: &str) -> usize {
592        match &self.config.partition_strategy {
593            PartitionStrategy::RoundRobin => {
594                // Use counter for round-robin
595                let mut counter = self.counter.write();
596                let partition = *counter % self.partitions.len();
597                *counter = counter.wrapping_add(1);
598                partition
599            }
600            PartitionStrategy::Hash { .. } => {
601                // Simple hash-based partitioning
602                let hash = self.hash_key(key);
603                (hash as usize) % self.partitions.len()
604            }
605            PartitionStrategy::ConsistentHash { .. } => {
606                // Simplified consistent hashing
607                let hash = self.hash_key(key);
608                (hash as usize) % self.partitions.len()
609            }
610            _ => 0, // Default to first partition
611        }
612    }
613
614    /// Simple hash function
615    fn hash_key(&self, key: &str) -> u64 {
616        use std::collections::hash_map::DefaultHasher;
617        use std::hash::{Hash, Hasher};
618
619        let mut hasher = DefaultHasher::new();
620        key.hash(&mut hasher);
621        hasher.finish()
622    }
623
624    /// Update partition load
625    pub fn update_partition_load(&self, partition_num: usize, load: f64) {
626        if let Some(mut partition) = self.partitions.get_mut(&partition_num) {
627            partition.load = load;
628            partition.last_updated = Utc::now();
629        }
630    }
631
632    /// Get partition count
633    pub fn partition_count(&self) -> usize {
634        self.partitions.len()
635    }
636
637    /// Get node count
638    pub fn node_count(&self) -> usize {
639        self.nodes.len()
640    }
641}
642
643/// Auto-scaler that monitors metrics and applies scaling decisions
644pub struct AutoScaler {
645    /// Configuration
646    config: ScalingConfig,
647    /// Partition manager
648    partition_manager: Arc<PartitionManager>,
649    /// Monitoring interval
650    monitoring_interval: Duration,
651    /// Command channel
652    command_tx: mpsc::UnboundedSender<ScalingCommand>,
653    /// Background task
654    _background_task: Option<tokio::task::JoinHandle<()>>,
655}
656
657/// Scaling command
658enum ScalingCommand {
659    Evaluate,
660    Stop,
661}
662
663impl AutoScaler {
664    /// Create a new auto-scaler
665    pub fn new(config: ScalingConfig, partition_manager: Arc<PartitionManager>) -> Self {
666        let (command_tx, mut command_rx) = mpsc::unbounded_channel();
667
668        let monitoring_interval = Duration::from_secs(30);
669
670        let partition_manager_clone = partition_manager.clone();
671        let background_task = tokio::spawn(async move {
672            let mut interval = tokio::time::interval(monitoring_interval);
673
674            loop {
675                tokio::select! {
676                    _ = interval.tick() => {
677                        let decision = partition_manager_clone.evaluate_scaling();
678                        if !matches!(decision, ScalingDirection::NoChange) {
679                            info!("Auto-scaler decision: {:?}", decision);
680                            if let Err(e) = partition_manager_clone.apply_scaling(&decision) {
681                                error!("Failed to apply scaling: {}", e);
682                            }
683                        }
684                    }
685                    Some(cmd) = command_rx.recv() => {
686                        match cmd {
687                            ScalingCommand::Evaluate => {
688                                let decision = partition_manager_clone.evaluate_scaling();
689                                if !matches!(decision, ScalingDirection::NoChange) {
690                                    if let Err(e) = partition_manager_clone.apply_scaling(&decision) {
691                                        error!("Failed to apply scaling: {}", e);
692                                    }
693                                }
694                            }
695                            ScalingCommand::Stop => break,
696                        }
697                    }
698                }
699            }
700        });
701
702        Self {
703            config,
704            partition_manager,
705            monitoring_interval,
706            command_tx,
707            _background_task: Some(background_task),
708        }
709    }
710
711    /// Trigger immediate evaluation
712    pub fn evaluate_now(&self) -> Result<()> {
713        self.command_tx
714            .send(ScalingCommand::Evaluate)
715            .map_err(|e| anyhow!("Failed to send command: {}", e))
716    }
717}
718
719#[cfg(test)]
720mod tests {
721    use super::*;
722
723    #[test]
724    fn test_adaptive_buffer() {
725        let config = ScalingConfig::default();
726        let buffer: AdaptiveBuffer<u64> = AdaptiveBuffer::new(config);
727
728        // Push items
729        for i in 0..100 {
730            buffer.push(i).unwrap();
731        }
732
733        assert_eq!(buffer.len(), 100);
734
735        // Pop items
736        for _ in 0..50 {
737            assert!(buffer.pop().is_some());
738        }
739
740        assert_eq!(buffer.len(), 50);
741    }
742
743    #[test]
744    fn test_partition_manager() {
745        let config = ScalingConfig::default();
746        let manager = PartitionManager::new(config);
747
748        // Add a node
749        let node = Node {
750            node_id: "node-1".to_string(),
751            address: "localhost:8001".to_string(),
752            partitions: Vec::new(),
753            resource_usage: ResourceUsage {
754                cpu_usage: 0.5,
755                memory_usage: 1024 * 1024 * 1024,
756                network_usage: 1000000,
757                events_per_second: 1000.0,
758            },
759            health: NodeHealth::Healthy,
760            last_heartbeat: Utc::now(),
761        };
762
763        manager.add_node(node).unwrap();
764
765        assert_eq!(manager.node_count(), 1);
766        assert!(manager.partition_count() >= 1);
767    }
768
769    #[test]
770    fn test_partition_assignment() {
771        let config = ScalingConfig::default();
772        let manager = PartitionManager::new(config);
773
774        let partition = manager.get_partition_for_key("test-key");
775        assert!(partition < manager.partition_count());
776    }
777}