Skip to main content

oxirs_stream/
multi_region_replication.rs

1//! # Multi-Region Replication
2//!
3//! Advanced multi-region replication system for OxiRS Stream providing global data consistency,
4//! failover capabilities, and optimized cross-region communication.
5
6use crate::StreamEvent;
7use anyhow::{anyhow, Result};
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use std::collections::{HashMap, HashSet, VecDeque};
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::sync::Arc;
13use std::time::{Duration, Instant};
14use tokio::sync::{Mutex, RwLock, Semaphore};
15use tokio::time;
16use tracing::{error, info, warn};
17use uuid::Uuid;
18
19/// Region configuration
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct RegionConfig {
22    /// Region identifier
23    pub region_id: String,
24    /// Region name (human-readable)
25    pub region_name: String,
26    /// Geographic location
27    pub location: GeographicLocation,
28    /// Network endpoints for this region
29    pub endpoints: Vec<RegionEndpoint>,
30    /// Replication priority (higher is more preferred)
31    pub priority: u8,
32    /// Whether this region is active for writes
33    pub is_write_active: bool,
34    /// Whether this region is active for reads
35    pub is_read_active: bool,
36    /// Replication mode for this region
37    pub replication_mode: ReplicationMode,
38    /// Network latency to other regions (ms)
39    pub latency_map: HashMap<String, u64>,
40}
41
42/// Geographic location information
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct GeographicLocation {
45    /// Country code (ISO 3166-1 alpha-2)
46    pub country: String,
47    /// Region/state/province
48    pub region: String,
49    /// City
50    pub city: String,
51    /// Latitude
52    pub latitude: f64,
53    /// Longitude
54    pub longitude: f64,
55    /// Availability zone (if applicable)
56    pub availability_zone: Option<String>,
57}
58
59/// Region endpoint configuration
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct RegionEndpoint {
62    /// Endpoint URL
63    pub url: String,
64    /// Endpoint type
65    pub endpoint_type: EndpointType,
66    /// Whether this endpoint is currently healthy
67    pub is_healthy: bool,
68    /// Last health check timestamp
69    pub last_health_check: Option<DateTime<Utc>>,
70    /// Authentication configuration
71    pub auth: Option<EndpointAuth>,
72}
73
74/// Endpoint type
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub enum EndpointType {
77    /// Primary streaming endpoint
78    Primary,
79    /// Secondary/backup endpoint
80    Secondary,
81    /// Administrative endpoint
82    Admin,
83    /// Health check endpoint
84    HealthCheck,
85}
86
87/// Endpoint authentication
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct EndpointAuth {
90    /// Authentication type
91    pub auth_type: String,
92    /// Credentials (encrypted)
93    pub credentials: HashMap<String, String>,
94}
95
96/// Replication mode
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub enum ReplicationMode {
99    /// Synchronous replication (wait for all regions)
100    Synchronous,
101    /// Asynchronous replication (fire and forget)
102    Asynchronous,
103    /// Semi-synchronous (wait for majority)
104    SemiSynchronous { min_replicas: usize },
105    /// Leader-follower (one primary region)
106    LeaderFollower { leader_region: String },
107    /// Active-active (all regions can write)
108    ActiveActive,
109}
110
111/// Replication configuration
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct ReplicationConfig {
114    /// Replication strategy
115    pub strategy: ReplicationStrategy,
116    /// Conflict resolution strategy
117    pub conflict_resolution: ConflictResolution,
118    /// Maximum replication lag tolerance
119    pub max_lag_ms: u64,
120    /// Replication timeout
121    pub replication_timeout: Duration,
122    /// Enable compression for cross-region traffic
123    pub enable_compression: bool,
124    /// Batch size for replication
125    pub batch_size: usize,
126    /// Health check interval
127    pub health_check_interval: Duration,
128    /// Failover timeout
129    pub failover_timeout: Duration,
130}
131
132/// Replication strategy
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub enum ReplicationStrategy {
135    /// Replicate all events to all regions
136    FullReplication,
137    /// Replicate only specific event types
138    SelectiveReplication { event_types: HashSet<String> },
139    /// Partition-based replication
140    PartitionBased {
141        partition_strategy: PartitionStrategy,
142    },
143    /// Geography-based replication
144    GeographyBased {
145        region_groups: HashMap<String, Vec<String>>,
146    },
147}
148
149/// Partition strategy for replication
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub enum PartitionStrategy {
152    /// Hash-based partitioning
153    Hash { hash_key: String },
154    /// Range-based partitioning
155    Range { ranges: Vec<PartitionRange> },
156    /// Custom partitioning logic
157    Custom { strategy_name: String },
158}
159
160/// Partition range definition
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct PartitionRange {
163    pub start: String,
164    pub end: String,
165    pub regions: Vec<String>,
166}
167
168/// Conflict resolution strategy
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub enum ConflictResolution {
171    /// Last write wins (timestamp-based)
172    LastWriteWins,
173    /// First write wins
174    FirstWriteWins,
175    /// Region priority based
176    RegionPriority { priority_order: Vec<String> },
177    /// Custom conflict resolution
178    Custom { resolver_name: String },
179    /// Manual resolution (queue conflicts)
180    Manual,
181}
182
183/// Replicated event with replication metadata
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct ReplicatedEvent {
186    /// Original event
187    pub event: StreamEvent,
188    /// Replication metadata
189    pub replication_metadata: ReplicationMetadata,
190}
191
192/// Replication metadata
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct ReplicationMetadata {
195    /// Unique replication ID
196    pub replication_id: Uuid,
197    /// Source region
198    pub source_region: String,
199    /// Target regions
200    pub target_regions: Vec<String>,
201    /// Replication timestamp
202    pub replication_timestamp: DateTime<Utc>,
203    /// Replication status per region
204    pub region_status: HashMap<String, ReplicationStatus>,
205    /// Vector clock for ordering
206    pub vector_clock: VectorClock,
207    /// Conflict resolution information
208    pub conflict_info: Option<ConflictInfo>,
209}
210
211/// Replication status for a region
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub enum ReplicationStatus {
214    /// Pending replication
215    Pending,
216    /// Successfully replicated
217    Success { timestamp: DateTime<Utc> },
218    /// Replication failed
219    Failed {
220        error: String,
221        timestamp: DateTime<Utc>,
222    },
223    /// Replication in progress
224    InProgress { started_at: DateTime<Utc> },
225}
226
227/// Vector clock for event ordering
228#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct VectorClock {
230    /// Clock values per region
231    pub clocks: HashMap<String, u64>,
232}
233
234impl Default for VectorClock {
235    fn default() -> Self {
236        Self::new()
237    }
238}
239
240impl VectorClock {
241    /// Create a new vector clock
242    pub fn new() -> Self {
243        Self {
244            clocks: HashMap::new(),
245        }
246    }
247
248    /// Increment clock for a region
249    pub fn increment(&mut self, region: &str) {
250        let current = self.clocks.get(region).unwrap_or(&0);
251        self.clocks.insert(region.to_string(), current + 1);
252    }
253
254    /// Update clock with another vector clock
255    pub fn update(&mut self, other: &VectorClock) {
256        for (region, other_clock) in &other.clocks {
257            let current = self.clocks.get(region).unwrap_or(&0);
258            self.clocks
259                .insert(region.clone(), (*current).max(*other_clock));
260        }
261    }
262
263    /// Check if this clock happens before another
264    pub fn happens_before(&self, other: &VectorClock) -> bool {
265        let mut strictly_less = false;
266
267        for region in self.clocks.keys().chain(other.clocks.keys()) {
268            let self_clock = self.clocks.get(region).unwrap_or(&0);
269            let other_clock = other.clocks.get(region).unwrap_or(&0);
270
271            if self_clock > other_clock {
272                return false; // Not happens-before
273            } else if self_clock < other_clock {
274                strictly_less = true;
275            }
276        }
277
278        strictly_less
279    }
280
281    /// Check if clocks are concurrent (neither happens before the other)
282    pub fn is_concurrent(&self, other: &VectorClock) -> bool {
283        !self.happens_before(other) && !other.happens_before(self)
284    }
285}
286
287/// Conflict information for resolution
288#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct ConflictInfo {
290    /// Conflict type
291    pub conflict_type: ConflictType,
292    /// Conflicting events
293    pub conflicting_events: Vec<StreamEvent>,
294    /// Resolution strategy used
295    pub resolution_strategy: ConflictResolution,
296    /// Resolution timestamp
297    pub resolved_at: Option<DateTime<Utc>>,
298    /// Resolution result
299    pub resolution_result: Option<StreamEvent>,
300}
301
302/// Type of conflict detected
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub enum ConflictType {
305    /// Write-write conflict (concurrent writes to same resource)
306    WriteWrite,
307    /// Write-read conflict
308    WriteRead,
309    /// Schema conflict
310    Schema,
311    /// Ordering conflict
312    Ordering,
313}
314
315/// Multi-region replication manager
316pub struct MultiRegionReplicationManager {
317    /// Replication configuration
318    config: ReplicationConfig,
319    /// Configured regions
320    regions: Arc<RwLock<HashMap<String, RegionConfig>>>,
321    /// Current region ID
322    current_region: String,
323    /// Replication statistics
324    stats: Arc<ReplicationStats>,
325    /// Conflict resolution queue
326    conflict_queue: Arc<Mutex<VecDeque<ConflictInfo>>>,
327    /// Vector clock for this region
328    vector_clock: Arc<Mutex<VectorClock>>,
329    /// Health monitoring
330    health_monitor: Arc<RegionHealthMonitor>,
331    /// Replication semaphore
332    replication_semaphore: Arc<Semaphore>,
333}
334
335/// Replication statistics
336#[derive(Debug, Default)]
337pub struct ReplicationStats {
338    pub total_events_replicated: AtomicU64,
339    pub successful_replications: AtomicU64,
340    pub failed_replications: AtomicU64,
341    pub conflicts_detected: AtomicU64,
342    pub conflicts_resolved: AtomicU64,
343    pub average_replication_latency_ms: AtomicU64,
344    pub cross_region_bandwidth_bytes: AtomicU64,
345    pub region_failures: AtomicU64,
346    pub failover_events: AtomicU64,
347}
348
349/// Region health monitor
350pub struct RegionHealthMonitor {
351    /// Health status per region
352    health_status: Arc<RwLock<HashMap<String, RegionHealth>>>,
353    /// Health check interval
354    check_interval: Duration,
355    /// Statistics
356    stats: Arc<HealthStats>,
357}
358
359/// Health status for a region
360#[derive(Debug, Clone)]
361pub struct RegionHealth {
362    /// Whether region is healthy
363    pub is_healthy: bool,
364    /// Last successful health check
365    pub last_success: Option<DateTime<Utc>>,
366    /// Last health check attempt
367    pub last_check: DateTime<Utc>,
368    /// Current latency to region
369    pub latency_ms: Option<u64>,
370    /// Error count in recent window
371    pub recent_errors: u32,
372    /// Health score (0.0 to 1.0)
373    pub health_score: f64,
374}
375
376/// Health monitoring statistics
377#[derive(Debug, Default)]
378pub struct HealthStats {
379    pub total_health_checks: AtomicU64,
380    pub failed_health_checks: AtomicU64,
381    pub average_latency_ms: AtomicU64,
382    pub regions_down: AtomicU64,
383}
384
385impl MultiRegionReplicationManager {
386    /// Create a new multi-region replication manager
387    pub fn new(config: ReplicationConfig, current_region: String) -> Self {
388        let health_monitor = Arc::new(RegionHealthMonitor::new(config.health_check_interval));
389
390        Self {
391            config,
392            current_region,
393            regions: Arc::new(RwLock::new(HashMap::new())),
394            stats: Arc::new(ReplicationStats::default()),
395            conflict_queue: Arc::new(Mutex::new(VecDeque::new())),
396            vector_clock: Arc::new(Mutex::new(VectorClock::new())),
397            health_monitor,
398            replication_semaphore: Arc::new(Semaphore::new(100)), // Max 100 concurrent replications
399        }
400    }
401
402    /// Add a region to the replication topology
403    pub async fn add_region(&self, region_config: RegionConfig) -> Result<()> {
404        let region_id = region_config.region_id.clone();
405        let mut regions = self.regions.write().await;
406        regions.insert(region_id.clone(), region_config);
407
408        // Initialize health monitoring for this region
409        self.health_monitor.add_region(region_id.clone()).await;
410
411        info!("Added region {} to replication topology", region_id);
412        Ok(())
413    }
414
415    /// Remove a region from the replication topology
416    pub async fn remove_region(&self, region_id: &str) -> Result<()> {
417        let mut regions = self.regions.write().await;
418        if regions.remove(region_id).is_some() {
419            self.health_monitor.remove_region(region_id).await;
420            info!("Removed region {} from replication topology", region_id);
421            Ok(())
422        } else {
423            Err(anyhow!("Region {} not found", region_id))
424        }
425    }
426
427    /// Replicate an event to other regions
428    pub async fn replicate_event(&self, event: StreamEvent) -> Result<ReplicatedEvent> {
429        let _permit = self.replication_semaphore.acquire().await?;
430        let start_time = Instant::now();
431
432        // Generate replication metadata
433        let mut vector_clock = self.vector_clock.lock().await;
434        vector_clock.increment(&self.current_region);
435        let replication_metadata = ReplicationMetadata {
436            replication_id: Uuid::new_v4(),
437            source_region: self.current_region.clone(),
438            target_regions: self.get_target_regions(&event).await?,
439            replication_timestamp: Utc::now(),
440            region_status: HashMap::new(),
441            vector_clock: vector_clock.clone(),
442            conflict_info: None,
443        };
444        drop(vector_clock);
445
446        let replicated_event = ReplicatedEvent {
447            event,
448            replication_metadata,
449        };
450
451        // Perform replication based on strategy
452        match self.config.strategy {
453            ReplicationStrategy::FullReplication => {
454                self.replicate_to_all_regions(&replicated_event).await?;
455            }
456            ReplicationStrategy::SelectiveReplication { ref event_types } => {
457                if self.should_replicate_event(&replicated_event.event, event_types) {
458                    self.replicate_to_all_regions(&replicated_event).await?;
459                }
460            }
461            ReplicationStrategy::PartitionBased {
462                ref partition_strategy,
463            } => {
464                self.replicate_partitioned(&replicated_event, partition_strategy)
465                    .await?;
466            }
467            ReplicationStrategy::GeographyBased { ref region_groups } => {
468                self.replicate_by_geography(&replicated_event, region_groups)
469                    .await?;
470            }
471        }
472
473        // Record statistics
474        let replication_latency = start_time.elapsed();
475        self.stats
476            .total_events_replicated
477            .fetch_add(1, Ordering::Relaxed);
478        self.stats
479            .average_replication_latency_ms
480            .store(replication_latency.as_millis() as u64, Ordering::Relaxed);
481
482        info!(
483            "Replicated event {} to {} regions in {:?}",
484            replicated_event.replication_metadata.replication_id,
485            replicated_event.replication_metadata.target_regions.len(),
486            replication_latency
487        );
488
489        Ok(replicated_event)
490    }
491
492    /// Handle an incoming replicated event
493    pub async fn handle_replicated_event(&self, replicated_event: ReplicatedEvent) -> Result<()> {
494        // Check for conflicts
495        if let Some(conflict) = self.detect_conflict(&replicated_event).await? {
496            self.handle_conflict(conflict).await?;
497            return Ok(());
498        }
499
500        // Update vector clock
501        let mut vector_clock = self.vector_clock.lock().await;
502        vector_clock.update(&replicated_event.replication_metadata.vector_clock);
503        drop(vector_clock);
504
505        // Process the event locally
506        self.process_replicated_event(replicated_event).await?;
507
508        Ok(())
509    }
510
511    /// Detect conflicts in replicated events
512    async fn detect_conflict(
513        &self,
514        replicated_event: &ReplicatedEvent,
515    ) -> Result<Option<ConflictInfo>> {
516        // Simple conflict detection based on vector clocks
517        let vector_clock = self.vector_clock.lock().await;
518
519        if vector_clock.is_concurrent(&replicated_event.replication_metadata.vector_clock) {
520            // Potential conflict detected
521            self.stats
522                .conflicts_detected
523                .fetch_add(1, Ordering::Relaxed);
524
525            let conflict_info = ConflictInfo {
526                conflict_type: ConflictType::WriteWrite,
527                conflicting_events: vec![replicated_event.event.clone()],
528                resolution_strategy: self.config.conflict_resolution.clone(),
529                resolved_at: None,
530                resolution_result: None,
531            };
532
533            warn!(
534                "Conflict detected for event {}",
535                replicated_event.replication_metadata.replication_id
536            );
537            return Ok(Some(conflict_info));
538        }
539
540        Ok(None)
541    }
542
543    /// Handle a detected conflict.
544    ///
545    /// Every strategy either produces a `resolution_result` or explicitly
546    /// queues the conflict for manual resolution — a conflict must never be
547    /// silently dropped on the floor.
548    async fn handle_conflict(&self, mut conflict_info: ConflictInfo) -> Result<()> {
549        match &self.config.conflict_resolution {
550            ConflictResolution::LastWriteWins => {
551                // Resolve by latest timestamp
552                conflict_info.resolution_result = Some(
553                    conflict_info
554                        .conflicting_events
555                        .iter()
556                        .max_by_key(|e| e.metadata().timestamp)
557                        .expect("conflicting_events should not be empty")
558                        .clone(),
559                );
560                conflict_info.resolved_at = Some(Utc::now());
561                self.stats
562                    .conflicts_resolved
563                    .fetch_add(1, Ordering::Relaxed);
564            }
565            ConflictResolution::FirstWriteWins => {
566                // Resolve by earliest timestamp
567                conflict_info.resolution_result = Some(
568                    conflict_info
569                        .conflicting_events
570                        .iter()
571                        .min_by_key(|e| e.metadata().timestamp)
572                        .expect("conflicting_events should not be empty")
573                        .clone(),
574                );
575                conflict_info.resolved_at = Some(Utc::now());
576                self.stats
577                    .conflicts_resolved
578                    .fetch_add(1, Ordering::Relaxed);
579            }
580            ConflictResolution::RegionPriority { priority_order } => {
581                // Pick the event whose originating region (carried in
582                // `EventMetadata::source`) ranks earliest in `priority_order`.
583                // Events from regions not listed are treated as lowest
584                // priority; ties (including "not listed" ties) are broken by
585                // latest timestamp (Last-Write-Wins as a secondary rule).
586                let rank = |event: &StreamEvent| -> usize {
587                    let source = &event.metadata().source;
588                    priority_order
589                        .iter()
590                        .position(|region| region == source)
591                        .unwrap_or(priority_order.len())
592                };
593                let resolved = conflict_info
594                    .conflicting_events
595                    .iter()
596                    .min_by(|a, b| {
597                        rank(a)
598                            .cmp(&rank(b))
599                            .then_with(|| b.metadata().timestamp.cmp(&a.metadata().timestamp))
600                    })
601                    .expect("conflicting_events should not be empty")
602                    .clone();
603                conflict_info.resolution_result = Some(resolved);
604                conflict_info.resolved_at = Some(Utc::now());
605                self.stats
606                    .conflicts_resolved
607                    .fetch_add(1, Ordering::Relaxed);
608            }
609            ConflictResolution::Custom { resolver_name } => {
610                // No pluggable custom-resolver mechanism exists yet. Rather
611                // than silently discarding the conflict, route it into the
612                // same manual-resolution queue as `Manual` and warn loudly
613                // so operators know a resolver they configured isn't wired up.
614                warn!(
615                    "Custom conflict resolver '{}' is not implemented; routing conflict {:?} to \
616                     the manual resolution queue instead of discarding it",
617                    resolver_name, conflict_info.conflict_type
618                );
619                let mut queue = self.conflict_queue.lock().await;
620                queue.push_back(conflict_info);
621            }
622            ConflictResolution::Manual => {
623                // Queue for manual resolution
624                let mut queue = self.conflict_queue.lock().await;
625                queue.push_back(conflict_info);
626            }
627        }
628
629        Ok(())
630    }
631
632    /// Get target regions for an event based on strategy
633    async fn get_target_regions(&self, _event: &StreamEvent) -> Result<Vec<String>> {
634        let regions = self.regions.read().await;
635        let healthy_regions = self.health_monitor.get_healthy_regions().await;
636
637        Ok(regions
638            .keys()
639            .filter(|region_id| {
640                **region_id != self.current_region && healthy_regions.contains(*region_id)
641            })
642            .cloned()
643            .collect())
644    }
645
646    /// Replicate to all available regions
647    async fn replicate_to_all_regions(&self, replicated_event: &ReplicatedEvent) -> Result<()> {
648        let regions = self.regions.read().await;
649        let mut replication_tasks = Vec::new();
650
651        for region_id in &replicated_event.replication_metadata.target_regions {
652            if let Some(region_config) = regions.get(region_id) {
653                let event_clone = replicated_event.clone();
654                let region_config_clone = region_config.clone();
655                let region_id_clone = region_id.clone();
656                let stats = self.stats.clone();
657
658                let task = tokio::spawn(async move {
659                    match Self::send_to_region(event_clone, region_config_clone).await {
660                        Ok(_) => {
661                            stats
662                                .successful_replications
663                                .fetch_add(1, Ordering::Relaxed);
664                        }
665                        Err(e) => {
666                            stats.failed_replications.fetch_add(1, Ordering::Relaxed);
667                            error!("Failed to replicate to region {}: {}", region_id_clone, e);
668                        }
669                    }
670                });
671
672                replication_tasks.push(task);
673            }
674        }
675
676        // Wait for replication based on mode
677        // Wait for all replications (can be optimized based on replication mode)
678        for task in replication_tasks {
679            let _ = task.await;
680        }
681
682        Ok(())
683    }
684
685    /// Send event to a specific region
686    async fn send_to_region(
687        _replicated_event: ReplicatedEvent,
688        _region_config: RegionConfig,
689    ) -> Result<()> {
690        // Simulate network call - in real implementation, this would use HTTP/gRPC
691        time::sleep(Duration::from_millis(50)).await;
692
693        // Simulate occasional failures
694        if fastrand::f32() < 0.05 {
695            // 5% failure rate
696            return Err(anyhow!("Simulated network failure"));
697        }
698
699        Ok(())
700    }
701
702    /// Check if an event should be replicated based on selective replication rules
703    fn should_replicate_event(&self, event: &StreamEvent, event_types: &HashSet<String>) -> bool {
704        let event_type = format!("{:?}", std::mem::discriminant(event));
705        event_types.contains(&event_type)
706    }
707
708    /// Replicate using partition-based strategy
709    async fn replicate_partitioned(
710        &self,
711        _replicated_event: &ReplicatedEvent,
712        _partition_strategy: &PartitionStrategy,
713    ) -> Result<()> {
714        // Implementation for partition-based replication
715        // This would determine which regions to replicate to based on partitioning
716        Ok(())
717    }
718
719    /// Replicate using geography-based strategy
720    async fn replicate_by_geography(
721        &self,
722        _replicated_event: &ReplicatedEvent,
723        _region_groups: &HashMap<String, Vec<String>>,
724    ) -> Result<()> {
725        // Implementation for geography-based replication
726        // This would replicate to regions in the same geographic group
727        Ok(())
728    }
729
730    /// Process a replicated event locally
731    async fn process_replicated_event(&self, _replicated_event: ReplicatedEvent) -> Result<()> {
732        // Implementation for processing replicated events locally
733        // This would typically integrate with the local storage system
734        Ok(())
735    }
736
737    /// Get replication statistics
738    pub fn get_stats(&self) -> ReplicationStats {
739        ReplicationStats {
740            total_events_replicated: AtomicU64::new(
741                self.stats.total_events_replicated.load(Ordering::Relaxed),
742            ),
743            successful_replications: AtomicU64::new(
744                self.stats.successful_replications.load(Ordering::Relaxed),
745            ),
746            failed_replications: AtomicU64::new(
747                self.stats.failed_replications.load(Ordering::Relaxed),
748            ),
749            conflicts_detected: AtomicU64::new(
750                self.stats.conflicts_detected.load(Ordering::Relaxed),
751            ),
752            conflicts_resolved: AtomicU64::new(
753                self.stats.conflicts_resolved.load(Ordering::Relaxed),
754            ),
755            average_replication_latency_ms: AtomicU64::new(
756                self.stats
757                    .average_replication_latency_ms
758                    .load(Ordering::Relaxed),
759            ),
760            cross_region_bandwidth_bytes: AtomicU64::new(
761                self.stats
762                    .cross_region_bandwidth_bytes
763                    .load(Ordering::Relaxed),
764            ),
765            region_failures: AtomicU64::new(self.stats.region_failures.load(Ordering::Relaxed)),
766            failover_events: AtomicU64::new(self.stats.failover_events.load(Ordering::Relaxed)),
767        }
768    }
769
770    /// Get conflicts waiting for manual resolution
771    pub async fn get_pending_conflicts(&self) -> Vec<ConflictInfo> {
772        let queue = self.conflict_queue.lock().await;
773        queue.iter().cloned().collect()
774    }
775}
776
777impl RegionHealthMonitor {
778    /// Create a new region health monitor
779    pub fn new(check_interval: Duration) -> Self {
780        Self {
781            health_status: Arc::new(RwLock::new(HashMap::new())),
782            check_interval,
783            stats: Arc::new(HealthStats::default()),
784        }
785    }
786
787    /// Add a region to monitor
788    pub async fn add_region(&self, region_id: String) {
789        let mut health_status = self.health_status.write().await;
790        health_status.insert(
791            region_id,
792            RegionHealth {
793                is_healthy: true,
794                last_success: None,
795                last_check: Utc::now(),
796                latency_ms: None,
797                recent_errors: 0,
798                health_score: 1.0,
799            },
800        );
801    }
802
803    /// Remove a region from monitoring
804    pub async fn remove_region(&self, region_id: &str) {
805        let mut health_status = self.health_status.write().await;
806        health_status.remove(region_id);
807    }
808
809    /// Get list of healthy regions
810    pub async fn get_healthy_regions(&self) -> Vec<String> {
811        let health_status = self.health_status.read().await;
812        health_status
813            .iter()
814            .filter(|(_, health)| health.is_healthy)
815            .map(|(region_id, _)| region_id.clone())
816            .collect()
817    }
818
819    /// Perform health check for all regions
820    pub async fn check_all_regions(&self) -> Result<()> {
821        let regions: Vec<String> = {
822            let health_status = self.health_status.read().await;
823            health_status.keys().cloned().collect()
824        };
825
826        for region_id in regions {
827            self.check_region_health(&region_id).await?;
828        }
829
830        Ok(())
831    }
832
833    /// Check health of a specific region
834    async fn check_region_health(&self, region_id: &str) -> Result<()> {
835        let start_time = Instant::now();
836        self.stats
837            .total_health_checks
838            .fetch_add(1, Ordering::Relaxed);
839
840        // Simulate health check - in real implementation, this would ping the region
841        let is_healthy = fastrand::f32() > 0.1; // 90% success rate
842        let latency = start_time.elapsed();
843
844        let mut health_status = self.health_status.write().await;
845        if let Some(health) = health_status.get_mut(region_id) {
846            health.last_check = Utc::now();
847            health.latency_ms = Some(latency.as_millis() as u64);
848
849            if is_healthy {
850                health.is_healthy = true;
851                health.last_success = Some(Utc::now());
852                health.recent_errors = 0;
853                health.health_score = (health.health_score + 0.1).min(1.0);
854            } else {
855                health.recent_errors += 1;
856                health.health_score = (health.health_score - 0.2).max(0.0);
857
858                if health.recent_errors > 3 {
859                    health.is_healthy = false;
860                    self.stats
861                        .failed_health_checks
862                        .fetch_add(1, Ordering::Relaxed);
863                }
864            }
865        }
866
867        Ok(())
868    }
869}
870
871#[cfg(test)]
872mod tests {
873    use super::*;
874    use crate::event::EventMetadata;
875    use std::collections::HashMap;
876
877    fn create_test_region(id: &str) -> RegionConfig {
878        RegionConfig {
879            region_id: id.to_string(),
880            region_name: format!("Region {id}"),
881            location: GeographicLocation {
882                country: "US".to_string(),
883                region: "California".to_string(),
884                city: "San Francisco".to_string(),
885                latitude: 37.7749,
886                longitude: -122.4194,
887                availability_zone: Some("us-west-1a".to_string()),
888            },
889            endpoints: vec![RegionEndpoint {
890                url: format!("https://{id}.example.com"),
891                endpoint_type: EndpointType::Primary,
892                is_healthy: true,
893                last_health_check: Some(Utc::now()),
894                auth: None,
895            }],
896            priority: 1,
897            is_write_active: true,
898            is_read_active: true,
899            replication_mode: ReplicationMode::Asynchronous,
900            latency_map: HashMap::new(),
901        }
902    }
903
904    fn create_test_event() -> StreamEvent {
905        StreamEvent::TripleAdded {
906            subject: "http://test.org/subject".to_string(),
907            predicate: "http://test.org/predicate".to_string(),
908            object: "\"test_value\"".to_string(),
909            graph: None,
910            metadata: EventMetadata {
911                event_id: Uuid::new_v4().to_string(),
912                timestamp: Utc::now(),
913                source: "test".to_string(),
914                user: None,
915                context: None,
916                caused_by: None,
917                version: "1.0".to_string(),
918                properties: HashMap::new(),
919                checksum: None,
920            },
921        }
922    }
923
924    #[tokio::test]
925    async fn test_replication_manager_creation() {
926        let config = ReplicationConfig {
927            strategy: ReplicationStrategy::FullReplication,
928            conflict_resolution: ConflictResolution::LastWriteWins,
929            max_lag_ms: 1000,
930            replication_timeout: Duration::from_secs(30),
931            enable_compression: true,
932            batch_size: 100,
933            health_check_interval: Duration::from_secs(60),
934            failover_timeout: Duration::from_secs(300),
935        };
936
937        let manager = MultiRegionReplicationManager::new(config, "us-west-1".to_string());
938        assert_eq!(manager.current_region, "us-west-1");
939    }
940
941    #[tokio::test]
942    async fn test_region_management() {
943        let config = ReplicationConfig {
944            strategy: ReplicationStrategy::FullReplication,
945            conflict_resolution: ConflictResolution::LastWriteWins,
946            max_lag_ms: 1000,
947            replication_timeout: Duration::from_secs(30),
948            enable_compression: true,
949            batch_size: 100,
950            health_check_interval: Duration::from_secs(60),
951            failover_timeout: Duration::from_secs(300),
952        };
953
954        let manager = MultiRegionReplicationManager::new(config, "us-west-1".to_string());
955
956        // Add regions
957        manager
958            .add_region(create_test_region("us-east-1"))
959            .await
960            .unwrap();
961        manager
962            .add_region(create_test_region("eu-west-1"))
963            .await
964            .unwrap();
965
966        let regions = manager.regions.read().await;
967        assert_eq!(regions.len(), 2);
968        assert!(regions.contains_key("us-east-1"));
969        assert!(regions.contains_key("eu-west-1"));
970    }
971
972    #[tokio::test]
973    async fn test_vector_clock() {
974        let mut clock1 = VectorClock::new();
975        let mut clock2 = VectorClock::new();
976
977        clock1.increment("region1");
978        clock2.increment("region2");
979
980        assert!(clock1.is_concurrent(&clock2));
981        assert!(!clock1.happens_before(&clock2));
982
983        clock1.update(&clock2);
984        clock1.increment("region1");
985
986        assert!(clock2.happens_before(&clock1));
987        assert!(!clock1.happens_before(&clock2));
988    }
989
990    #[tokio::test]
991    async fn test_health_monitor() {
992        let monitor = RegionHealthMonitor::new(Duration::from_secs(60));
993
994        monitor.add_region("us-west-1".to_string()).await;
995        monitor.add_region("us-east-1".to_string()).await;
996
997        let healthy_regions = monitor.get_healthy_regions().await;
998        assert_eq!(healthy_regions.len(), 2);
999
1000        monitor.check_all_regions().await.unwrap();
1001
1002        let stats = &monitor.stats;
1003        assert!(stats.total_health_checks.load(Ordering::Relaxed) >= 2);
1004    }
1005
1006    #[test]
1007    fn test_replication_config() {
1008        let config = ReplicationConfig {
1009            strategy: ReplicationStrategy::SelectiveReplication {
1010                event_types: ["TripleAdded", "TripleRemoved"]
1011                    .iter()
1012                    .map(|s| s.to_string())
1013                    .collect(),
1014            },
1015            conflict_resolution: ConflictResolution::RegionPriority {
1016                priority_order: vec!["us-west-1".to_string(), "us-east-1".to_string()],
1017            },
1018            max_lag_ms: 500,
1019            replication_timeout: Duration::from_secs(15),
1020            enable_compression: false,
1021            batch_size: 50,
1022            health_check_interval: Duration::from_secs(30),
1023            failover_timeout: Duration::from_secs(120),
1024        };
1025
1026        match config.strategy {
1027            ReplicationStrategy::SelectiveReplication { ref event_types } => {
1028                assert_eq!(event_types.len(), 2);
1029            }
1030            _ => panic!("Wrong strategy type"),
1031        }
1032    }
1033
1034    fn make_replication_config(conflict_resolution: ConflictResolution) -> ReplicationConfig {
1035        ReplicationConfig {
1036            strategy: ReplicationStrategy::FullReplication,
1037            conflict_resolution,
1038            max_lag_ms: 1000,
1039            replication_timeout: Duration::from_secs(30),
1040            enable_compression: true,
1041            batch_size: 100,
1042            health_check_interval: Duration::from_secs(60),
1043            failover_timeout: Duration::from_secs(300),
1044        }
1045    }
1046
1047    fn event_with(source: &str, timestamp: DateTime<Utc>) -> StreamEvent {
1048        let mut event = create_test_event();
1049        if let StreamEvent::TripleAdded { metadata, .. } = &mut event {
1050            metadata.source = source.to_string();
1051            metadata.timestamp = timestamp;
1052        }
1053        event
1054    }
1055
1056    /// Regression test for multi_region_replication.rs:561 — `FirstWriteWins`
1057    /// used to fall into the catch-all `_ => warn!(...)` arm and never
1058    /// resolve or count the conflict at all. It must now actually resolve
1059    /// (reflected by `conflicts_resolved`) instead of being silently dropped.
1060    #[tokio::test]
1061    async fn test_handle_conflict_first_write_wins_resolves() {
1062        let manager = MultiRegionReplicationManager::new(
1063            make_replication_config(ConflictResolution::FirstWriteWins),
1064            "us-west-1".to_string(),
1065        );
1066
1067        let now = Utc::now();
1068        let conflict = ConflictInfo {
1069            conflict_type: ConflictType::WriteWrite,
1070            conflicting_events: vec![
1071                event_with("us-east-1", now),
1072                event_with("us-west-1", now - chrono::Duration::seconds(10)),
1073            ],
1074            resolution_strategy: ConflictResolution::FirstWriteWins,
1075            resolved_at: None,
1076            resolution_result: None,
1077        };
1078
1079        manager.handle_conflict(conflict).await.unwrap();
1080
1081        assert_eq!(
1082            manager.stats.conflicts_resolved.load(Ordering::Relaxed),
1083            1,
1084            "FirstWriteWins must actually resolve the conflict, not silently drop it"
1085        );
1086        assert!(
1087            manager.get_pending_conflicts().await.is_empty(),
1088            "a resolved conflict must not also sit in the manual-resolution queue"
1089        );
1090    }
1091
1092    /// Regression test for multi_region_replication.rs:561 — `RegionPriority`
1093    /// used to be silently dropped by the same catch-all arm. It must now
1094    /// pick the event from the highest-priority listed region and count as
1095    /// resolved.
1096    #[tokio::test]
1097    async fn test_handle_conflict_region_priority_resolves() {
1098        let manager = MultiRegionReplicationManager::new(
1099            make_replication_config(ConflictResolution::RegionPriority {
1100                priority_order: vec!["us-west-1".to_string(), "us-east-1".to_string()],
1101            }),
1102            "us-west-1".to_string(),
1103        );
1104
1105        let now = Utc::now();
1106        // The lower-priority region's event is newer, so a naive
1107        // Last-Write-Wins fallback would pick it; RegionPriority must still
1108        // prefer "us-west-1" because it's first in `priority_order`.
1109        let conflict = ConflictInfo {
1110            conflict_type: ConflictType::WriteWrite,
1111            conflicting_events: vec![
1112                event_with("us-east-1", now),
1113                event_with("us-west-1", now - chrono::Duration::seconds(10)),
1114            ],
1115            resolution_strategy: ConflictResolution::RegionPriority {
1116                priority_order: vec!["us-west-1".to_string(), "us-east-1".to_string()],
1117            },
1118            resolved_at: None,
1119            resolution_result: None,
1120        };
1121
1122        manager.handle_conflict(conflict).await.unwrap();
1123
1124        assert_eq!(
1125            manager.stats.conflicts_resolved.load(Ordering::Relaxed),
1126            1,
1127            "RegionPriority must actually resolve the conflict, not silently drop it"
1128        );
1129    }
1130
1131    /// Regression test for multi_region_replication.rs:561 — `Custom` used to
1132    /// be silently dropped (no queueing, no resolution). It must now be
1133    /// routed into the manual-resolution queue instead of being discarded.
1134    #[tokio::test]
1135    async fn test_handle_conflict_custom_routes_to_manual_queue() {
1136        let manager = MultiRegionReplicationManager::new(
1137            make_replication_config(ConflictResolution::Custom {
1138                resolver_name: "not-yet-implemented".to_string(),
1139            }),
1140            "us-west-1".to_string(),
1141        );
1142
1143        let conflict = ConflictInfo {
1144            conflict_type: ConflictType::WriteWrite,
1145            conflicting_events: vec![event_with("us-east-1", Utc::now())],
1146            resolution_strategy: ConflictResolution::Custom {
1147                resolver_name: "not-yet-implemented".to_string(),
1148            },
1149            resolved_at: None,
1150            resolution_result: None,
1151        };
1152
1153        manager.handle_conflict(conflict).await.unwrap();
1154
1155        let pending = manager.get_pending_conflicts().await;
1156        assert_eq!(
1157            pending.len(),
1158            1,
1159            "an unimplemented Custom resolver must route the conflict to the manual queue, \
1160             not discard it"
1161        );
1162    }
1163}