anya_core/infrastructure/high_availability/
cluster.rs

1use crate::infrastructure::high_availability::config::HighAvailabilityConfig;
2use crate::infrastructure::high_availability::{ClusterStatus, FailoverPhase, HaError};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::sync::Arc;
6use tokio::sync::RwLock;
7use tracing::{debug, error, info, warn};
8use uuid::Uuid;
9
10/// Manager for cluster operations and coordination
11/// [AIR-3][AIS-3][AIT-3][PFM-3][SCL-3][RES-3]
12pub struct ClusterManager {
13    config: Arc<HighAvailabilityConfig>,
14    nodes: Arc<RwLock<HashMap<String, NodeInfo>>>,
15    node_id: String,
16    current_leader: Arc<RwLock<Option<String>>>,
17    status: Arc<RwLock<ClusterStatus>>,
18    discovery_service: Box<dyn NodeDiscovery + Send + Sync>,
19    membership_service: Box<dyn ClusterMembership + Send + Sync>,
20}
21
22impl ClusterManager {
23    /// Creates a new cluster manager
24    pub fn new(config: &HighAvailabilityConfig) -> Self {
25        let node_id = Uuid::new_v4().to_string();
26        let discovery_service = create_discovery_service(config);
27        let membership_service = create_membership_service(config);
28
29        Self {
30            config: Arc::new(config.clone()),
31            nodes: Arc::new(RwLock::new(HashMap::new())),
32            node_id,
33            current_leader: Arc::new(RwLock::new(None)),
34            status: Arc::new(RwLock::new(ClusterStatus::Initializing)),
35            discovery_service,
36            membership_service,
37        }
38    }
39
40    /// Initializes the cluster manager
41    pub async fn initialize(&mut self) -> Result<(), HaError> {
42        info!("Initializing cluster manager");
43
44        // Discover initial nodes
45        let discovered_nodes = self
46            .discovery_service
47            .discover_nodes()
48            .await
49            .map_err(|e| HaError::ClusterError(format!("Failed to discover nodes: {e}")))?;
50
51        // Initialize node list
52        let mut nodes = self.nodes.write().await;
53        for node_addr in discovered_nodes {
54            let node_info = NodeInfo {
55                id: node_addr.clone(), // Temporary ID until we get real info
56                address: node_addr,
57                status: NodeStatus::Unknown,
58                role: NodeRole::Unknown,
59                last_heartbeat: None,
60                metadata: HashMap::new(),
61            };
62            nodes.insert(node_info.id.clone(), node_info);
63        }
64
65        // Add self node
66        let self_node = NodeInfo {
67            id: self.node_id.clone(),
68            address: self
69                .membership_service
70                .get_local_address()
71                .map_err(|e| HaError::ClusterError(format!("Failed to get local address: {e}")))?,
72            status: NodeStatus::Starting,
73            role: NodeRole::Follower, // Start as follower
74            last_heartbeat: Some(chrono::Utc::now()),
75            metadata: HashMap::new(),
76        };
77        nodes.insert(self.node_id.clone(), self_node);
78
79        // Initialize membership service
80        self.membership_service.initialize().await.map_err(|e| {
81            HaError::ClusterError(format!("Failed to initialize membership service: {e}"))
82        })?;
83
84        *self.status.write().await = ClusterStatus::Initializing;
85
86        debug!("Cluster manager initialized with {} nodes", nodes.len());
87        Ok(())
88    }
89
90    /// Joins the cluster
91    pub async fn join_cluster(&mut self) -> Result<(), HaError> {
92        info!("Joining cluster {}", self.config.cluster.cluster_name);
93
94        // Register with the membership service
95        self.membership_service
96            .join(self.node_id.clone())
97            .await
98            .map_err(|e| HaError::ClusterError(format!("Failed to join cluster: {e}")))?;
99
100        // Update node status
101        let mut nodes = self.nodes.write().await;
102        if let Some(node) = nodes.get_mut(&self.node_id) {
103            node.status = NodeStatus::Active;
104        }
105
106        // Start leader election if needed
107        let leader = self.current_leader.read().await;
108        if leader.is_none() {
109            drop(leader);
110            drop(nodes);
111            self.elect_leader().await?;
112        }
113
114        // Update cluster status
115        let mut status = self.status.write().await;
116        *status = ClusterStatus::Healthy;
117
118        info!(
119            "Successfully joined cluster {}",
120            self.config.cluster.cluster_name
121        );
122        Ok(())
123    }
124
125    /// Leaves the cluster
126    pub async fn leave_cluster(&mut self) -> Result<(), HaError> {
127        info!("Leaving cluster {}", self.config.cluster.cluster_name);
128
129        // Unregister from the membership service
130        self.membership_service
131            .leave(self.node_id.clone())
132            .await
133            .map_err(|e| HaError::ClusterError(format!("Failed to leave cluster: {e}")))?;
134
135        // Update node status
136        let mut nodes = self.nodes.write().await;
137        if let Some(node) = nodes.get_mut(&self.node_id) {
138            node.status = NodeStatus::Leaving;
139        }
140
141        // Update cluster status
142        let mut status = self.status.write().await;
143        *status = ClusterStatus::Down {
144            reason: "Node left the cluster".to_string(),
145        };
146
147        info!(
148            "Successfully left cluster {}",
149            self.config.cluster.cluster_name
150        );
151        Ok(())
152    }
153
154    /// Gets the current cluster status
155    pub async fn get_status(&self) -> Result<ClusterStatus, HaError> {
156        let status = self.status.read().await;
157        Ok(status.clone())
158    }
159
160    /// Updates the cluster configuration
161    pub async fn update_config(&mut self, config: &HighAvailabilityConfig) -> Result<(), HaError> {
162        info!("Updating cluster configuration");
163        self.config = Arc::new(config.clone());
164
165        // Update discovery service if needed
166        if self.discovery_service.needs_update(config) {
167            self.discovery_service = create_discovery_service(config);
168        }
169
170        // Update membership service if needed
171        if self.membership_service.needs_update(config) {
172            let old_membership = std::mem::replace(
173                &mut self.membership_service,
174                create_membership_service(config),
175            );
176            old_membership
177                .leave(self.node_id.clone())
178                .await
179                .map_err(|e| {
180                    HaError::ClusterError(format!("Failed to leave old membership: {e}"))
181                })?;
182
183            self.membership_service.initialize().await.map_err(|e| {
184                HaError::ClusterError(format!("Failed to initialize new membership: {e}"))
185            })?;
186
187            self.membership_service
188                .join(self.node_id.clone())
189                .await
190                .map_err(|e| {
191                    HaError::ClusterError(format!("Failed to join with new membership: {e}"))
192                })?;
193        }
194
195        Ok(())
196    }
197
198    /// Elects a new leader for the cluster
199    async fn elect_leader(&mut self) -> Result<(), HaError> {
200        info!("Starting leader election");
201
202        // Simple leader election strategy - pick the node with the lowest ID
203        let nodes = self.nodes.read().await;
204
205        // Filter to only include active nodes
206        let active_nodes: Vec<&NodeInfo> = nodes
207            .values()
208            .filter(|n| n.status == NodeStatus::Active)
209            .collect();
210
211        if active_nodes.is_empty() {
212            return Err(HaError::ClusterError(
213                "No active nodes for leader election".to_string(),
214            ));
215        }
216
217        // Pick the node with the lowest ID as leader
218        let new_leader = active_nodes
219            .iter()
220            .min_by_key(|n| &n.id)
221            .map(|n| n.id.clone())
222            .ok_or_else(|| {
223                HaError::ClusterError("No active nodes available for leader election".to_string())
224            })?;
225
226        // Update leader
227        let mut leader = self.current_leader.write().await;
228        *leader = Some(new_leader.clone());
229
230        // Update node roles
231        drop(leader);
232        drop(nodes);
233        let mut nodes = self.nodes.write().await;
234
235        for (id, node) in nodes.iter_mut() {
236            if node.status == NodeStatus::Active {
237                if *id == new_leader {
238                    node.role = NodeRole::Leader;
239                } else {
240                    node.role = NodeRole::Follower;
241                }
242            }
243        }
244
245        info!("Leader election completed, new leader: {}", new_leader);
246        Ok(())
247    }
248
249    /// Processes a heartbeat from a node
250    pub async fn process_heartbeat(&mut self, node_id: &str) -> Result<(), HaError> {
251        let mut nodes = self.nodes.write().await;
252
253        if let Some(node) = nodes.get_mut(node_id) {
254            node.last_heartbeat = Some(chrono::Utc::now());
255            if node.status == NodeStatus::Unknown || node.status == NodeStatus::Suspected {
256                node.status = NodeStatus::Active;
257            }
258        } else {
259            // New node discovered
260            let node_info = NodeInfo {
261                id: node_id.to_string(),
262                address: node_id.to_string(), // Will be updated with real address later
263                status: NodeStatus::Active,
264                role: NodeRole::Follower,
265                last_heartbeat: Some(chrono::Utc::now()),
266                metadata: HashMap::new(),
267            };
268            nodes.insert(node_id.to_string(), node_info);
269        }
270
271        Ok(())
272    }
273
274    /// Checks and updates node health based on heartbeats
275    pub async fn check_node_health(&mut self) -> Result<(), HaError> {
276        debug!("Checking node health");
277
278        let now = chrono::Utc::now();
279        let node_timeout = chrono::Duration::from_std(self.config.cluster.node_timeout)
280            .map_err(|_| HaError::ClusterError("Invalid node timeout duration".to_string()))?;
281
282        let mut nodes = self.nodes.write().await;
283        let mut active_count = 0;
284        let mut total_count = 0;
285        let mut failures = Vec::new();
286
287        for (id, node) in nodes.iter_mut() {
288            if id == &self.node_id {
289                // Skip self
290                continue;
291            }
292
293            total_count += 1;
294
295            if let Some(last_heartbeat) = node.last_heartbeat {
296                let elapsed = now - last_heartbeat;
297
298                if elapsed > node_timeout {
299                    // Node timeout
300                    if node.status == NodeStatus::Active {
301                        warn!(
302                            "Node {} has not sent heartbeat in {:?}, marking as suspected",
303                            id, elapsed
304                        );
305                        node.status = NodeStatus::Suspected;
306                    } else if node.status == NodeStatus::Suspected {
307                        error!("Node {} has not recovered, marking as failed", id);
308                        node.status = NodeStatus::Failed;
309                        failures.push(id.clone());
310                    }
311                } else if node.status == NodeStatus::Active {
312                    active_count += 1;
313                }
314            }
315        }
316
317        // Update cluster status based on node health
318        drop(nodes);
319
320        let mut status = self.status.write().await;
321        if !failures.is_empty() {
322            // We have node failures, update status
323            if active_count < total_count && active_count > 0 {
324                *status = ClusterStatus::Degraded {
325                    active_nodes: active_count,
326                    total_nodes: total_count,
327                    details: format!("Nodes failed: {}", failures.join(", ")),
328                };
329            } else if active_count == 0 {
330                *status = ClusterStatus::Down {
331                    reason: "All nodes failed".to_string(),
332                };
333            }
334
335            // Check if we need to trigger failover
336            let leader = self.current_leader.read().await;
337            if let Some(leader_id) = leader.as_ref() {
338                if failures.contains(leader_id) {
339                    // Leader failed, initiate failover
340                    let leader_id_clone = leader_id.clone();
341                    drop(leader);
342                    drop(status);
343
344                    // Update status to failover
345                    let mut status = self.status.write().await;
346                    *status = ClusterStatus::Failover {
347                        primary_node: leader_id_clone.clone(),
348                        failing_node: Some(leader_id_clone.clone()),
349                        failover_phase: FailoverPhase::Detection,
350                    };
351
352                    // We don't trigger the failover directly here,
353                    // the failover manager will handle this
354                    info!(
355                        "Leader node {} failed, cluster entering failover state",
356                        leader_id_clone
357                    );
358                }
359            }
360        } else if active_count == total_count {
361            // All nodes are healthy
362            *status = ClusterStatus::Healthy;
363        }
364
365        Ok(())
366    }
367
368    /// Gets the list of current nodes
369    pub async fn get_nodes(&self) -> Result<Vec<NodeInfo>, HaError> {
370        let nodes = self.nodes.read().await;
371        Ok(nodes.values().cloned().collect())
372    }
373
374    /// Gets the current leader node
375    pub async fn get_leader(&self) -> Result<Option<String>, HaError> {
376        let leader = self.current_leader.read().await;
377        Ok(leader.clone())
378    }
379
380    /// Checks if this node is the current leader
381    pub async fn is_leader(&self) -> Result<bool, HaError> {
382        let leader = self.current_leader.read().await;
383        Ok(leader.as_ref() == Some(&self.node_id))
384    }
385}
386
387/// Creates an appropriate discovery service based on configuration
388fn create_discovery_service(
389    config: &HighAvailabilityConfig,
390) -> Box<dyn NodeDiscovery + Send + Sync> {
391    match config.cluster.discovery_method {
392        crate::infrastructure::high_availability::config::DiscoveryMethod::Static => {
393            Box::new(StaticDiscovery::new(config.cluster.static_nodes.clone()))
394        }
395        crate::infrastructure::high_availability::config::DiscoveryMethod::Dns => {
396            if let Some(ref dns_url) = config.cluster.dns_discovery_url {
397                Box::new(DnsDiscovery::new(dns_url.clone()))
398            } else {
399                // Fallback to static if DNS URL is not provided
400                Box::new(StaticDiscovery::new(config.cluster.static_nodes.clone()))
401            }
402        }
403        crate::infrastructure::high_availability::config::DiscoveryMethod::Kubernetes => {
404            if let Some(ref service_name) = config.cluster.k8s_service_name {
405                Box::new(KubernetesDiscovery::new(service_name.clone()))
406            } else {
407                // Fallback to static if K8s service name is not provided
408                Box::new(StaticDiscovery::new(config.cluster.static_nodes.clone()))
409            }
410        }
411        _ => {
412            // Fallback to static for other methods
413            Box::new(StaticDiscovery::new(config.cluster.static_nodes.clone()))
414        }
415    }
416}
417
418/// Creates an appropriate membership service based on configuration
419fn create_membership_service(
420    config: &HighAvailabilityConfig,
421) -> Box<dyn ClusterMembership + Send + Sync> {
422    // For simplicity, always use the basic membership service
423    Box::new(BasicMembership::new(config))
424}
425
426/// Information about a cluster node
427#[derive(Debug, Clone, Serialize, Deserialize)]
428pub struct NodeInfo {
429    /// Unique identifier for the node
430    pub id: String,
431
432    /// Network address of the node
433    pub address: String,
434
435    /// Current status of the node
436    pub status: NodeStatus,
437
438    /// Role of the node in the cluster
439    pub role: NodeRole,
440
441    /// Time of last heartbeat received
442    pub last_heartbeat: Option<chrono::DateTime<chrono::Utc>>,
443
444    /// Additional metadata about the node
445    pub metadata: HashMap<String, String>,
446}
447
448/// Status of a node
449#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
450pub enum NodeStatus {
451    /// Node status is unknown
452    Unknown,
453
454    /// Node is starting up
455    Starting,
456
457    /// Node is active and healthy
458    Active,
459
460    /// Node is suspected of being down
461    Suspected,
462
463    /// Node has failed
464    Failed,
465
466    /// Node is leaving the cluster
467    Leaving,
468}
469
470/// Role of a node in the cluster
471#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
472pub enum NodeRole {
473    /// Role is unknown
474    Unknown,
475
476    /// Node is a leader/primary
477    Leader,
478
479    /// Node is a follower/replica
480    Follower,
481
482    /// Node is an observer (non-voting)
483    Observer,
484}
485
486/// Trait for node discovery mechanisms
487#[async_trait::async_trait]
488pub trait NodeDiscovery {
489    /// Discovers nodes in the cluster
490    async fn discover_nodes(&self) -> Result<Vec<String>, String>;
491
492    /// Checks if this discovery service needs to be updated
493    fn needs_update(&self, config: &HighAvailabilityConfig) -> bool;
494}
495
496/// Static node discovery
497pub struct StaticDiscovery {
498    nodes: Vec<String>,
499}
500
501impl StaticDiscovery {
502    pub fn new(nodes: Vec<String>) -> Self {
503        Self { nodes }
504    }
505}
506
507#[async_trait::async_trait]
508impl NodeDiscovery for StaticDiscovery {
509    async fn discover_nodes(&self) -> Result<Vec<String>, String> {
510        Ok(self.nodes.clone())
511    }
512
513    fn needs_update(&self, config: &HighAvailabilityConfig) -> bool {
514        self.nodes != config.cluster.static_nodes
515    }
516}
517
518/// DNS-based node discovery
519pub struct DnsDiscovery {
520    dns_url: String,
521}
522
523impl DnsDiscovery {
524    pub fn new(dns_url: String) -> Self {
525        Self { dns_url }
526    }
527}
528
529#[async_trait::async_trait]
530impl NodeDiscovery for DnsDiscovery {
531    async fn discover_nodes(&self) -> Result<Vec<String>, String> {
532        // In a real implementation, this would perform DNS SRV lookups
533        // For now, just return a mock response
534        Ok(vec![
535            format!("node1.{}", self.dns_url),
536            format!("node2.{}", self.dns_url),
537            format!("node3.{}", self.dns_url),
538        ])
539    }
540
541    fn needs_update(&self, config: &HighAvailabilityConfig) -> bool {
542        if let Some(ref dns_url) = config.cluster.dns_discovery_url {
543            self.dns_url != *dns_url
544        } else {
545            true
546        }
547    }
548}
549
550/// Kubernetes-based node discovery
551pub struct KubernetesDiscovery {
552    service_name: String,
553}
554
555impl KubernetesDiscovery {
556    pub fn new(service_name: String) -> Self {
557        Self { service_name }
558    }
559}
560
561#[async_trait::async_trait]
562impl NodeDiscovery for KubernetesDiscovery {
563    async fn discover_nodes(&self) -> Result<Vec<String>, String> {
564        // In a real implementation, this would query the Kubernetes API
565        // For now, just return a mock response
566        Ok(vec![
567            format!("{}-0.{}", self.service_name, self.service_name),
568            format!("{}-1.{}", self.service_name, self.service_name),
569            format!("{}-2.{}", self.service_name, self.service_name),
570        ])
571    }
572
573    fn needs_update(&self, config: &HighAvailabilityConfig) -> bool {
574        if let Some(ref service_name) = config.cluster.k8s_service_name {
575            self.service_name != *service_name
576        } else {
577            true
578        }
579    }
580}
581
582/// Trait for cluster membership
583#[async_trait::async_trait]
584pub trait ClusterMembership {
585    /// Initializes the membership service
586    async fn initialize(&mut self) -> Result<(), String>;
587
588    /// Joins the cluster
589    async fn join(&self, node_id: String) -> Result<(), String>;
590
591    /// Leaves the cluster
592    async fn leave(&self, node_id: String) -> Result<(), String>;
593
594    /// Gets the local node address
595    fn get_local_address(&self) -> Result<String, String>;
596
597    /// Checks if this membership service needs to be updated
598    fn needs_update(&self, _config: &HighAvailabilityConfig) -> bool {
599        false
600    }
601}
602
603/// Basic cluster membership implementation
604pub struct BasicMembership {
605    config: HighAvailabilityConfig,
606    local_address: String,
607}
608
609impl BasicMembership {
610    pub fn new(config: &HighAvailabilityConfig) -> Self {
611        // In a real implementation, this would determine the local address dynamically
612        let local_address = "127.0.0.1:5001".to_string();
613
614        Self {
615            config: config.clone(),
616            local_address,
617        }
618    }
619}
620
621#[async_trait::async_trait]
622impl ClusterMembership for BasicMembership {
623    async fn initialize(&mut self) -> Result<(), String> {
624        // In a real implementation, this would set up network connections
625        Ok(())
626    }
627
628    async fn join(&self, node_id: String) -> Result<(), String> {
629        info!(
630            "Node {} joining cluster {}",
631            node_id, self.config.cluster.cluster_name
632        );
633        // In a real implementation, this would register with other nodes
634        Ok(())
635    }
636
637    async fn leave(&self, node_id: String) -> Result<(), String> {
638        info!(
639            "Node {} leaving cluster {}",
640            node_id, self.config.cluster.cluster_name
641        );
642        // In a real implementation, this would deregister from other nodes
643        Ok(())
644    }
645
646    fn get_local_address(&self) -> Result<String, String> {
647        Ok(self.local_address.clone())
648    }
649}