anya_core/infrastructure/high_availability/
failover.rs

1use serde::{Deserialize, Serialize};
2use std::sync::Arc;
3use std::time::Duration;
4use tokio::sync::RwLock;
5use tokio::time::Instant;
6use tracing::{debug, info, instrument, warn};
7
8use crate::infrastructure::high_availability::{
9    config::HighAvailabilityConfig, FailoverPhase, HaError,
10};
11
12/// Failover manager implementing automatic failover patterns
13/// Follows the Leader and Followers pattern from distributed systems
14/// [AIR-3][AIS-3][RES-3]
15#[derive(Debug)]
16pub struct FailoverManager {
17    config: Arc<HighAvailabilityConfig>,
18    current_phase: Arc<RwLock<FailoverPhase>>,
19    failover_history: Arc<RwLock<Vec<FailoverEvent>>>,
20    enabled: Arc<RwLock<bool>>,
21    last_failover_attempt: Arc<RwLock<Option<Instant>>>,
22}
23
24/// Represents a failover event in the system
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct FailoverEvent {
27    pub id: String,
28    pub timestamp: chrono::DateTime<chrono::Utc>,
29    pub trigger_reason: String,
30    pub source_node: Option<String>,
31    pub target_node: Option<String>,
32    pub phase: FailoverPhase,
33    pub duration_ms: Option<u64>,
34    pub success: bool,
35    pub error: Option<String>,
36}
37
38/// Failover triggers
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum FailoverTrigger {
41    /// Node failure detected
42    NodeFailure(String),
43    /// Health check failure
44    HealthCheckFailure(String),
45    /// Manual failover requested
46    Manual,
47    /// Performance degradation
48    PerformanceDegradation(String),
49    /// Network partition
50    NetworkPartition,
51}
52
53impl FailoverManager {
54    /// Creates a new failover manager
55    pub fn new(config: &HighAvailabilityConfig) -> Self {
56        Self {
57            config: Arc::new(config.clone()),
58            current_phase: Arc::new(RwLock::new(FailoverPhase::Completed)),
59            failover_history: Arc::new(RwLock::new(Vec::new())),
60            enabled: Arc::new(RwLock::new(config.failover.enabled)),
61            last_failover_attempt: Arc::new(RwLock::new(None)),
62        }
63    }
64
65    /// Initializes the failover manager
66    #[instrument(skip(self))]
67    pub async fn initialize(&mut self) -> Result<(), HaError> {
68        info!("Initializing failover manager");
69
70        if !self.config.failover.enabled {
71            warn!("Failover is disabled in configuration");
72            *self.enabled.write().await = false;
73            return Ok(());
74        }
75
76        *self.enabled.write().await = true;
77        *self.current_phase.write().await = FailoverPhase::Completed;
78
79        info!("Failover manager initialized");
80        Ok(())
81    }
82
83    /// Enables the failover manager
84    #[instrument(skip(self))]
85    pub async fn enable(&mut self) -> Result<(), HaError> {
86        info!("Enabling failover manager");
87        *self.enabled.write().await = true;
88        Ok(())
89    }
90
91    /// Disables the failover manager
92    #[instrument(skip(self))]
93    pub async fn disable(&mut self) -> Result<(), HaError> {
94        info!("Disabling failover manager");
95        *self.enabled.write().await = false;
96        Ok(())
97    }
98
99    /// Triggers a manual failover
100    #[instrument(skip(self))]
101    pub async fn trigger_manual_failover(&mut self) -> Result<(), HaError> {
102        self.trigger_failover(FailoverTrigger::Manual, None, None)
103            .await
104    }
105
106    /// Triggers failover for a specific reason
107    #[instrument(skip(self))]
108    pub async fn trigger_failover(
109        &mut self,
110        trigger: FailoverTrigger,
111        source_node: Option<String>,
112        target_node: Option<String>,
113    ) -> Result<(), HaError> {
114        if !*self.enabled.read().await {
115            warn!("Failover is disabled, ignoring trigger: {:?}", trigger);
116            return Ok(());
117        }
118
119        let current_phase = *self.current_phase.read().await;
120        if current_phase != FailoverPhase::Completed {
121            warn!(
122                "Failover already in progress (phase: {:?}), ignoring new trigger: {:?}",
123                current_phase, trigger
124            );
125            return Err(HaError::FailoverError(
126                "Failover already in progress".to_string(),
127            ));
128        }
129
130        // Check rate limiting
131        if let Some(last_attempt) = *self.last_failover_attempt.read().await {
132            let elapsed = last_attempt.elapsed();
133            let min_interval = Duration::from_secs(30); // Minimum 30 seconds between failovers
134
135            if elapsed < min_interval {
136                warn!("Failover rate limited, last attempt was {:?} ago", elapsed);
137                return Err(HaError::FailoverError("Failover rate limited".to_string()));
138            }
139        }
140
141        info!("Triggering failover: {:?}", trigger);
142        *self.last_failover_attempt.write().await = Some(Instant::now());
143
144        let event_id = uuid::Uuid::new_v4().to_string();
145        let start_time = Instant::now();
146
147        // Execute failover phases
148        let result = self
149            .execute_failover_sequence(
150                event_id.clone(),
151                trigger.clone(),
152                source_node.clone(),
153                target_node.clone(),
154                start_time,
155            )
156            .await;
157
158        // Record the event
159        let duration = start_time.elapsed().as_millis() as u64;
160        let (success, error) = match &result {
161            Ok(_) => (true, None),
162            Err(e) => (false, Some(e.to_string())),
163        };
164
165        let event = FailoverEvent {
166            id: event_id,
167            timestamp: chrono::Utc::now(),
168            trigger_reason: format!("{trigger:?}"),
169            source_node,
170            target_node,
171            phase: *self.current_phase.read().await,
172            duration_ms: Some(duration),
173            success,
174            error,
175        };
176
177        self.failover_history.write().await.push(event);
178
179        // Ensure we're back to completed state
180        *self.current_phase.write().await = FailoverPhase::Completed;
181
182        result
183    }
184
185    /// Executes the complete failover sequence
186    async fn execute_failover_sequence(
187        &mut self,
188        event_id: String,
189        trigger: FailoverTrigger,
190        source_node: Option<String>,
191        target_node: Option<String>,
192        start_time: Instant,
193    ) -> Result<(), HaError> {
194        // Phase 1: Detection
195        *self.current_phase.write().await = FailoverPhase::Detection;
196        info!("Failover {}: Detection phase", event_id);
197        self.detect_failure(&trigger).await?;
198
199        // Phase 2: Election
200        *self.current_phase.write().await = FailoverPhase::Election;
201        info!("Failover {}: Election phase", event_id);
202        let new_leader = self
203            .elect_new_leader(source_node.as_deref(), target_node.as_deref())
204            .await?;
205
206        // Phase 3: Promotion
207        *self.current_phase.write().await = FailoverPhase::Promotion;
208        info!(
209            "Failover {}: Promotion phase - promoting {}",
210            event_id, new_leader
211        );
212        self.promote_new_leader(&new_leader).await?;
213
214        // Phase 4: Redirection
215        *self.current_phase.write().await = FailoverPhase::Redirection;
216        info!("Failover {}: Redirection phase", event_id);
217        self.redirect_traffic(&new_leader).await?;
218
219        // Phase 5: Recovery (optional, for failed node)
220        if let Some(failed_node) = &source_node {
221            *self.current_phase.write().await = FailoverPhase::Recovery;
222            info!("Failover {}: Recovery phase for {}", event_id, failed_node);
223            self.initiate_recovery(failed_node).await?;
224        }
225
226        info!(
227            "Failover {} completed successfully in {}ms",
228            event_id,
229            start_time.elapsed().as_millis()
230        );
231
232        Ok(())
233    }
234
235    /// Detects and validates the failure
236    async fn detect_failure(&self, trigger: &FailoverTrigger) -> Result<(), HaError> {
237        debug!("Detecting failure: {:?}", trigger);
238
239        match trigger {
240            FailoverTrigger::NodeFailure(node) => {
241                // Verify the node is actually down
242                if !self.verify_node_failure(node).await? {
243                    return Err(HaError::FailoverError(format!(
244                        "Node {node} appears to be healthy"
245                    )));
246                }
247            }
248            FailoverTrigger::HealthCheckFailure(component) => {
249                // Verify health check failure is critical
250                if !self.verify_health_failure(component).await? {
251                    return Err(HaError::FailoverError(format!(
252                        "Component {component} health check not critical"
253                    )));
254                }
255            }
256            FailoverTrigger::Manual => {
257                // Manual failover always proceeds
258                info!("Manual failover requested");
259            }
260            FailoverTrigger::PerformanceDegradation(reason) => {
261                debug!("Performance degradation detected: {}", reason);
262            }
263            FailoverTrigger::NetworkPartition => {
264                debug!("Network partition detected");
265            }
266        }
267
268        Ok(())
269    }
270
271    /// Elects a new leader node
272    async fn elect_new_leader(
273        &self,
274        failed_node: Option<&str>,
275        preferred_node: Option<&str>,
276    ) -> Result<String, HaError> {
277        debug!("Electing new leader");
278
279        // If a preferred node is specified, use it
280        if let Some(node) = preferred_node {
281            info!("Using preferred node as new leader: {}", node);
282            return Ok(node.to_string());
283        }
284
285        // In a real implementation, this would:
286        // 1. Query available nodes
287        // 2. Check their health and eligibility
288        // 3. Apply election algorithm (e.g., highest priority, least loaded)
289        // 4. Ensure consensus among nodes
290
291        // For now, simulate election
292        let available_nodes = self.get_available_nodes(failed_node).await?;
293
294        if available_nodes.is_empty() {
295            return Err(HaError::FailoverError(
296                "No available nodes for promotion".to_string(),
297            ));
298        }
299
300        // Select the first available node (in real implementation, use proper election logic)
301        let new_leader = available_nodes[0].clone();
302        info!("Elected new leader: {}", new_leader);
303
304        Ok(new_leader)
305    }
306
307    /// Promotes a node to leader
308    async fn promote_new_leader(&self, node: &str) -> Result<(), HaError> {
309        debug!("Promoting {} to leader", node);
310
311        // In a real implementation, this would:
312        // 1. Update the node's role to leader
313        // 2. Initialize leader-specific services
314        // 3. Update cluster metadata
315        // 4. Notify other nodes
316
317        tokio::time::sleep(Duration::from_millis(100)).await; // Simulate promotion time
318
319        info!("Successfully promoted {} to leader", node);
320        Ok(())
321    }
322
323    /// Redirects traffic to the new leader
324    async fn redirect_traffic(&self, new_leader: &str) -> Result<(), HaError> {
325        debug!("Redirecting traffic to {}", new_leader);
326
327        // In a real implementation, this would:
328        // 1. Update load balancer configuration
329        // 2. Update DNS records
330        // 3. Notify clients
331        // 4. Update service discovery
332
333        tokio::time::sleep(Duration::from_millis(50)).await; // Simulate redirection time
334
335        info!("Successfully redirected traffic to {}", new_leader);
336        Ok(())
337    }
338
339    /// Initiates recovery for a failed node
340    async fn initiate_recovery(&self, failed_node: &str) -> Result<(), HaError> {
341        debug!("Initiating recovery for {}", failed_node);
342
343        // In a real implementation, this would:
344        // 1. Try to restart the node
345        // 2. Check if it can rejoin the cluster
346        // 3. Sync any missed data
347        // 4. Update its role appropriately
348
349        info!("Recovery initiated for {}", failed_node);
350        Ok(())
351    }
352
353    /// Verifies that a node has actually failed
354    async fn verify_node_failure(&self, node: &str) -> Result<bool, HaError> {
355        debug!("Verifying failure of node: {}", node);
356
357        // In a real implementation, this would:
358        // 1. Try to ping the node
359        // 2. Check heartbeat timestamps
360        // 3. Verify with other nodes
361        // 4. Check network connectivity
362
363        // For simulation, assume the node is indeed failed
364        Ok(true)
365    }
366
367    /// Verifies that a health check failure is critical
368    async fn verify_health_failure(&self, component: &str) -> Result<bool, HaError> {
369        debug!("Verifying health failure of component: {}", component);
370
371        // In a real implementation, this would:
372        // 1. Check the severity of the health failure
373        // 2. Verify with multiple health checks
374        // 3. Check if it affects critical functionality
375
376        // For simulation, assume it's critical
377        Ok(true)
378    }
379
380    /// Gets list of available nodes for promotion
381    async fn get_available_nodes(
382        &self,
383        exclude_node: Option<&str>,
384    ) -> Result<Vec<String>, HaError> {
385        // In a real implementation, this would query the cluster manager
386        let mut nodes = vec![
387            "node-1".to_string(),
388            "node-2".to_string(),
389            "node-3".to_string(),
390        ];
391
392        // Remove the failed node
393        if let Some(failed) = exclude_node {
394            nodes.retain(|n| n != failed);
395        }
396
397        Ok(nodes)
398    }
399
400    /// Updates the failover manager configuration
401    #[instrument(skip(self, config))]
402    pub async fn update_config(&mut self, config: &HighAvailabilityConfig) -> Result<(), HaError> {
403        info!("Updating failover manager configuration");
404        self.config = Arc::new(config.clone());
405        *self.enabled.write().await = config.failover.enabled;
406        Ok(())
407    }
408
409    /// Gets the current failover phase
410    pub async fn get_current_phase(&self) -> FailoverPhase {
411        *self.current_phase.read().await
412    }
413
414    /// Gets the failover history
415    pub async fn get_failover_history(&self) -> Vec<FailoverEvent> {
416        self.failover_history.read().await.clone()
417    }
418
419    /// Checks if failover is currently active
420    pub async fn is_failover_active(&self) -> bool {
421        *self.current_phase.read().await != FailoverPhase::Completed
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428    use crate::infrastructure::high_availability::config::FailoverConfig;
429
430    fn create_test_config() -> HighAvailabilityConfig {
431        HighAvailabilityConfig {
432            failover: FailoverConfig {
433                enabled: true,
434                auto_failover: true,
435                failover_timeout: Duration::from_secs(30),
436                min_nodes_for_failover: 2,
437                max_auto_failovers: Some(3),
438                auto_failover_period: Duration::from_secs(3600),
439                fencing_enabled: true,
440            },
441            ..Default::default()
442        }
443    }
444
445    #[tokio::test]
446    async fn test_failover_manager_creation() {
447        let config = create_test_config();
448        let manager = FailoverManager::new(&config);
449
450        assert!(!manager.is_failover_active().await);
451        assert_eq!(manager.get_current_phase().await, FailoverPhase::Completed);
452    }
453
454    #[tokio::test]
455    async fn test_enable_disable() {
456        let config = create_test_config();
457        let mut manager = FailoverManager::new(&config);
458
459        manager.initialize().await.unwrap();
460        assert!(*manager.enabled.read().await);
461
462        manager.disable().await.unwrap();
463        assert!(!*manager.enabled.read().await);
464
465        manager.enable().await.unwrap();
466        assert!(*manager.enabled.read().await);
467    }
468
469    #[tokio::test]
470    async fn test_manual_failover() {
471        let config = create_test_config();
472        let mut manager = FailoverManager::new(&config);
473
474        manager.initialize().await.unwrap();
475
476        let result = manager.trigger_manual_failover().await;
477        assert!(result.is_ok());
478
479        let history = manager.get_failover_history().await;
480        assert_eq!(history.len(), 1);
481        assert!(history[0].success);
482    }
483}