Skip to main content

forge_orchestration/federation/
replication.rs

1//! Cross-region replication for data and workloads
2//!
3//! Implements replication strategies for:
4//! - Active-passive failover
5//! - Active-active multi-region
6//! - Data synchronization
7
8use std::collections::{HashMap, HashSet};
9use std::sync::Arc;
10use parking_lot::RwLock;
11use serde::{Deserialize, Serialize};
12use tracing::{debug, info, warn};
13
14/// Replication policy
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub enum ReplicationPolicy {
17    /// No replication
18    None,
19    /// Replicate to specific regions
20    Explicit {
21        /// Target region names.
22        regions: Vec<String>,
23    },
24    /// Replicate to N regions
25    Count {
26        /// Number of regions to replicate to.
27        count: usize,
28    },
29    /// Replicate to all regions
30    All,
31    /// Replicate based on topology (e.g., one per zone)
32    Topology {
33        /// Topology label key to spread across.
34        key: String,
35        /// Number of replicas per distinct key value.
36        count_per_key: usize,
37    },
38}
39
40impl Default for ReplicationPolicy {
41    fn default() -> Self {
42        Self::None
43    }
44}
45
46/// Replication status for a resource
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct ReplicationStatus {
49    /// Resource ID
50    pub resource_id: String,
51    /// Primary region
52    pub primary_region: String,
53    /// Replica regions
54    pub replica_regions: Vec<String>,
55    /// Replication state per region
56    pub region_states: HashMap<String, ReplicaState>,
57    /// Last sync time
58    pub last_sync: chrono::DateTime<chrono::Utc>,
59    /// Replication lag in milliseconds
60    pub lag_ms: HashMap<String, u64>,
61}
62
63/// State of a replica
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65pub enum ReplicaState {
66    /// Replica is in sync
67    InSync,
68    /// Replica is syncing
69    Syncing,
70    /// Replica is lagging
71    Lagging,
72    /// Replica has failed
73    Failed,
74    /// Replica is being created
75    Creating,
76    /// Replica is being deleted
77    Deleting,
78}
79
80/// Replication controller for managing cross-region replication
81pub struct ReplicationController {
82    /// Replication status by resource
83    status: Arc<RwLock<HashMap<String, ReplicationStatus>>>,
84    /// Default replication policy
85    default_policy: RwLock<ReplicationPolicy>,
86    /// Available regions
87    regions: RwLock<HashSet<String>>,
88    /// Replication callbacks
89    callbacks: RwLock<Vec<Arc<dyn ReplicationCallback + Send + Sync>>>,
90}
91
92/// Callback for replication events
93pub trait ReplicationCallback: Send + Sync {
94    /// Called when replication is initiated
95    fn on_replicate(&self, resource_id: &str, source: &str, target: &str);
96    
97    /// Called when replication completes
98    fn on_sync_complete(&self, resource_id: &str, region: &str);
99    
100    /// Called when replication fails
101    fn on_sync_failed(&self, resource_id: &str, region: &str, error: &str);
102}
103
104impl ReplicationController {
105    /// Create new replication controller
106    pub fn new() -> Self {
107        Self {
108            status: Arc::new(RwLock::new(HashMap::new())),
109            default_policy: RwLock::new(ReplicationPolicy::None),
110            regions: RwLock::new(HashSet::new()),
111            callbacks: RwLock::new(Vec::new()),
112        }
113    }
114
115    /// Set default replication policy
116    pub fn set_default_policy(&self, policy: ReplicationPolicy) {
117        *self.default_policy.write() = policy;
118    }
119
120    /// Register available region
121    pub fn register_region(&self, region: impl Into<String>) {
122        self.regions.write().insert(region.into());
123    }
124
125    /// Unregister region
126    pub fn unregister_region(&self, region: &str) {
127        self.regions.write().remove(region);
128    }
129
130    /// Add replication callback
131    pub fn add_callback<C: ReplicationCallback + 'static>(&self, callback: C) {
132        self.callbacks.write().push(Arc::new(callback));
133    }
134
135    /// Start replication for a resource
136    pub fn replicate(&self, resource_id: impl Into<String>, primary_region: impl Into<String>, policy: Option<ReplicationPolicy>) {
137        let resource_id = resource_id.into();
138        let primary_region = primary_region.into();
139        let policy = policy.unwrap_or_else(|| self.default_policy.read().clone());
140
141        let target_regions = self.resolve_target_regions(&primary_region, &policy);
142        
143        if target_regions.is_empty() {
144            debug!(resource_id = %resource_id, "No replication targets");
145            return;
146        }
147
148        info!(
149            resource_id = %resource_id,
150            primary = %primary_region,
151            targets = ?target_regions,
152            "Starting replication"
153        );
154
155        // Initialize status
156        let mut region_states = HashMap::new();
157        region_states.insert(primary_region.clone(), ReplicaState::InSync);
158        
159        for region in &target_regions {
160            region_states.insert(region.clone(), ReplicaState::Creating);
161        }
162
163        let status = ReplicationStatus {
164            resource_id: resource_id.clone(),
165            primary_region: primary_region.clone(),
166            replica_regions: target_regions.clone(),
167            region_states,
168            last_sync: chrono::Utc::now(),
169            lag_ms: HashMap::new(),
170        };
171
172        self.status.write().insert(resource_id.clone(), status);
173
174        // Notify callbacks
175        let callbacks = self.callbacks.read();
176        for region in &target_regions {
177            for callback in callbacks.iter() {
178                callback.on_replicate(&resource_id, &primary_region, region);
179            }
180        }
181    }
182
183    /// Resolve target regions based on policy
184    fn resolve_target_regions(&self, primary: &str, policy: &ReplicationPolicy) -> Vec<String> {
185        let regions = self.regions.read();
186        let available: Vec<_> = regions.iter()
187            .filter(|r| *r != primary)
188            .cloned()
189            .collect();
190
191        match policy {
192            ReplicationPolicy::None => Vec::new(),
193            ReplicationPolicy::Explicit { regions: targets } => {
194                targets.iter()
195                    .filter(|r| available.contains(r))
196                    .cloned()
197                    .collect()
198            }
199            ReplicationPolicy::Count { count } => {
200                available.into_iter().take(*count).collect()
201            }
202            ReplicationPolicy::All => available,
203            ReplicationPolicy::Topology { key, count_per_key } => {
204                // Simplified - in real implementation would check topology labels
205                available.into_iter().take(*count_per_key).collect()
206            }
207        }
208    }
209
210    /// Update replica state
211    pub fn update_state(&self, resource_id: &str, region: &str, state: ReplicaState) {
212        let mut statuses = self.status.write();
213        
214        if let Some(status) = statuses.get_mut(resource_id) {
215            let old_state = status.region_states.get(region).copied();
216            status.region_states.insert(region.to_string(), state);
217
218            // Notify on state changes
219            if old_state != Some(state) {
220                let callbacks = self.callbacks.read();
221                
222                match state {
223                    ReplicaState::InSync => {
224                        for callback in callbacks.iter() {
225                            callback.on_sync_complete(resource_id, region);
226                        }
227                    }
228                    ReplicaState::Failed => {
229                        for callback in callbacks.iter() {
230                            callback.on_sync_failed(resource_id, region, "Replication failed");
231                        }
232                    }
233                    _ => {}
234                }
235            }
236        }
237    }
238
239    /// Update replication lag
240    pub fn update_lag(&self, resource_id: &str, region: &str, lag_ms: u64) {
241        let mut statuses = self.status.write();
242        
243        if let Some(status) = statuses.get_mut(resource_id) {
244            status.lag_ms.insert(region.to_string(), lag_ms);
245            status.last_sync = chrono::Utc::now();
246
247            // Update state based on lag
248            let state = if lag_ms == 0 {
249                ReplicaState::InSync
250            } else if lag_ms < 1000 {
251                ReplicaState::Syncing
252            } else {
253                ReplicaState::Lagging
254            };
255
256            status.region_states.insert(region.to_string(), state);
257        }
258    }
259
260    /// Get replication status
261    pub fn get_status(&self, resource_id: &str) -> Option<ReplicationStatus> {
262        self.status.read().get(resource_id).cloned()
263    }
264
265    /// Get all replicated resources
266    pub fn list_replicated(&self) -> Vec<ReplicationStatus> {
267        self.status.read().values().cloned().collect()
268    }
269
270    /// Stop replication for a resource
271    pub fn stop_replication(&self, resource_id: &str) {
272        if let Some(mut status) = self.status.write().remove(resource_id) {
273            info!(resource_id = %resource_id, "Stopping replication");
274            
275            // Mark all replicas as deleting
276            for (region, state) in status.region_states.iter_mut() {
277                if region != &status.primary_region {
278                    *state = ReplicaState::Deleting;
279                }
280            }
281        }
282    }
283
284    /// Promote a replica to primary
285    pub fn promote(&self, resource_id: &str, new_primary: &str) -> bool {
286        let mut statuses = self.status.write();
287        
288        if let Some(status) = statuses.get_mut(resource_id) {
289            if !status.replica_regions.contains(&new_primary.to_string()) 
290                && status.primary_region != new_primary {
291                warn!(
292                    resource_id = %resource_id,
293                    new_primary = %new_primary,
294                    "Cannot promote: region is not a replica"
295                );
296                return false;
297            }
298
299            let old_primary = status.primary_region.clone();
300            status.primary_region = new_primary.to_string();
301            
302            // Update replica list
303            status.replica_regions.retain(|r| r != new_primary);
304            if !status.replica_regions.contains(&old_primary) {
305                status.replica_regions.push(old_primary);
306            }
307
308            info!(
309                resource_id = %resource_id,
310                old_primary = %status.replica_regions.last().unwrap_or(&String::new()),
311                new_primary = %new_primary,
312                "Promoted replica to primary"
313            );
314
315            return true;
316        }
317
318        false
319    }
320
321    /// Check if resource is replicated to a region
322    pub fn is_replicated_to(&self, resource_id: &str, region: &str) -> bool {
323        self.status.read()
324            .get(resource_id)
325            .map(|s| s.primary_region == region || s.replica_regions.contains(&region.to_string()))
326            .unwrap_or(false)
327    }
328
329    /// Get healthy replicas for a resource
330    pub fn healthy_replicas(&self, resource_id: &str) -> Vec<String> {
331        self.status.read()
332            .get(resource_id)
333            .map(|s| {
334                let mut healthy = vec![s.primary_region.clone()];
335                healthy.extend(
336                    s.region_states.iter()
337                        .filter(|(r, state)| {
338                            *r != &s.primary_region && **state == ReplicaState::InSync
339                        })
340                        .map(|(r, _)| r.clone())
341                );
342                healthy
343            })
344            .unwrap_or_default()
345    }
346}
347
348impl Default for ReplicationController {
349    fn default() -> Self {
350        Self::new()
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357
358    #[test]
359    fn test_replication_policy() {
360        let controller = ReplicationController::new();
361        
362        controller.register_region("us-east-1");
363        controller.register_region("us-west-2");
364        controller.register_region("eu-west-1");
365
366        controller.replicate(
367            "resource-1",
368            "us-east-1",
369            Some(ReplicationPolicy::Count { count: 2 })
370        );
371
372        let status = controller.get_status("resource-1").unwrap();
373        assert_eq!(status.primary_region, "us-east-1");
374        assert_eq!(status.replica_regions.len(), 2);
375    }
376
377    #[test]
378    fn test_replica_promotion() {
379        let controller = ReplicationController::new();
380        
381        controller.register_region("us-east-1");
382        controller.register_region("eu-west-1");
383
384        controller.replicate(
385            "resource-1",
386            "us-east-1",
387            Some(ReplicationPolicy::All)
388        );
389
390        // Mark replica as in sync
391        controller.update_state("resource-1", "eu-west-1", ReplicaState::InSync);
392
393        // Promote
394        assert!(controller.promote("resource-1", "eu-west-1"));
395
396        let status = controller.get_status("resource-1").unwrap();
397        assert_eq!(status.primary_region, "eu-west-1");
398        assert!(status.replica_regions.contains(&"us-east-1".to_string()));
399    }
400
401    #[test]
402    fn test_healthy_replicas() {
403        let controller = ReplicationController::new();
404        
405        controller.register_region("us-east-1");
406        controller.register_region("us-west-2");
407        controller.register_region("eu-west-1");
408
409        controller.replicate(
410            "resource-1",
411            "us-east-1",
412            Some(ReplicationPolicy::All)
413        );
414
415        controller.update_state("resource-1", "us-west-2", ReplicaState::InSync);
416        controller.update_state("resource-1", "eu-west-1", ReplicaState::Failed);
417
418        let healthy = controller.healthy_replicas("resource-1");
419        assert!(healthy.contains(&"us-east-1".to_string()));
420        assert!(healthy.contains(&"us-west-2".to_string()));
421        assert!(!healthy.contains(&"eu-west-1".to_string()));
422    }
423}