1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct RegionConfig {
22 pub region_id: String,
24 pub region_name: String,
26 pub location: GeographicLocation,
28 pub endpoints: Vec<RegionEndpoint>,
30 pub priority: u8,
32 pub is_write_active: bool,
34 pub is_read_active: bool,
36 pub replication_mode: ReplicationMode,
38 pub latency_map: HashMap<String, u64>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct GeographicLocation {
45 pub country: String,
47 pub region: String,
49 pub city: String,
51 pub latitude: f64,
53 pub longitude: f64,
55 pub availability_zone: Option<String>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct RegionEndpoint {
62 pub url: String,
64 pub endpoint_type: EndpointType,
66 pub is_healthy: bool,
68 pub last_health_check: Option<DateTime<Utc>>,
70 pub auth: Option<EndpointAuth>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub enum EndpointType {
77 Primary,
79 Secondary,
81 Admin,
83 HealthCheck,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct EndpointAuth {
90 pub auth_type: String,
92 pub credentials: HashMap<String, String>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub enum ReplicationMode {
99 Synchronous,
101 Asynchronous,
103 SemiSynchronous { min_replicas: usize },
105 LeaderFollower { leader_region: String },
107 ActiveActive,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct ReplicationConfig {
114 pub strategy: ReplicationStrategy,
116 pub conflict_resolution: ConflictResolution,
118 pub max_lag_ms: u64,
120 pub replication_timeout: Duration,
122 pub enable_compression: bool,
124 pub batch_size: usize,
126 pub health_check_interval: Duration,
128 pub failover_timeout: Duration,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134pub enum ReplicationStrategy {
135 FullReplication,
137 SelectiveReplication { event_types: HashSet<String> },
139 PartitionBased {
141 partition_strategy: PartitionStrategy,
142 },
143 GeographyBased {
145 region_groups: HashMap<String, Vec<String>>,
146 },
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub enum PartitionStrategy {
152 Hash { hash_key: String },
154 Range { ranges: Vec<PartitionRange> },
156 Custom { strategy_name: String },
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct PartitionRange {
163 pub start: String,
164 pub end: String,
165 pub regions: Vec<String>,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub enum ConflictResolution {
171 LastWriteWins,
173 FirstWriteWins,
175 RegionPriority { priority_order: Vec<String> },
177 Custom { resolver_name: String },
179 Manual,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct ReplicatedEvent {
186 pub event: StreamEvent,
188 pub replication_metadata: ReplicationMetadata,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct ReplicationMetadata {
195 pub replication_id: Uuid,
197 pub source_region: String,
199 pub target_regions: Vec<String>,
201 pub replication_timestamp: DateTime<Utc>,
203 pub region_status: HashMap<String, ReplicationStatus>,
205 pub vector_clock: VectorClock,
207 pub conflict_info: Option<ConflictInfo>,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
213pub enum ReplicationStatus {
214 Pending,
216 Success { timestamp: DateTime<Utc> },
218 Failed {
220 error: String,
221 timestamp: DateTime<Utc>,
222 },
223 InProgress { started_at: DateTime<Utc> },
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct VectorClock {
230 pub clocks: HashMap<String, u64>,
232}
233
234impl Default for VectorClock {
235 fn default() -> Self {
236 Self::new()
237 }
238}
239
240impl VectorClock {
241 pub fn new() -> Self {
243 Self {
244 clocks: HashMap::new(),
245 }
246 }
247
248 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 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 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; } else if self_clock < other_clock {
274 strictly_less = true;
275 }
276 }
277
278 strictly_less
279 }
280
281 pub fn is_concurrent(&self, other: &VectorClock) -> bool {
283 !self.happens_before(other) && !other.happens_before(self)
284 }
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct ConflictInfo {
290 pub conflict_type: ConflictType,
292 pub conflicting_events: Vec<StreamEvent>,
294 pub resolution_strategy: ConflictResolution,
296 pub resolved_at: Option<DateTime<Utc>>,
298 pub resolution_result: Option<StreamEvent>,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
304pub enum ConflictType {
305 WriteWrite,
307 WriteRead,
309 Schema,
311 Ordering,
313}
314
315pub struct MultiRegionReplicationManager {
317 config: ReplicationConfig,
319 regions: Arc<RwLock<HashMap<String, RegionConfig>>>,
321 current_region: String,
323 stats: Arc<ReplicationStats>,
325 conflict_queue: Arc<Mutex<VecDeque<ConflictInfo>>>,
327 vector_clock: Arc<Mutex<VectorClock>>,
329 health_monitor: Arc<RegionHealthMonitor>,
331 replication_semaphore: Arc<Semaphore>,
333}
334
335#[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
349pub struct RegionHealthMonitor {
351 health_status: Arc<RwLock<HashMap<String, RegionHealth>>>,
353 check_interval: Duration,
355 stats: Arc<HealthStats>,
357}
358
359#[derive(Debug, Clone)]
361pub struct RegionHealth {
362 pub is_healthy: bool,
364 pub last_success: Option<DateTime<Utc>>,
366 pub last_check: DateTime<Utc>,
368 pub latency_ms: Option<u64>,
370 pub recent_errors: u32,
372 pub health_score: f64,
374}
375
376#[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 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)), }
400 }
401
402 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 self.health_monitor.add_region(region_id.clone()).await;
410
411 info!("Added region {} to replication topology", region_id);
412 Ok(())
413 }
414
415 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 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 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 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 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 pub async fn handle_replicated_event(&self, replicated_event: ReplicatedEvent) -> Result<()> {
494 if let Some(conflict) = self.detect_conflict(&replicated_event).await? {
496 self.handle_conflict(conflict).await?;
497 return Ok(());
498 }
499
500 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 self.process_replicated_event(replicated_event).await?;
507
508 Ok(())
509 }
510
511 async fn detect_conflict(
513 &self,
514 replicated_event: &ReplicatedEvent,
515 ) -> Result<Option<ConflictInfo>> {
516 let vector_clock = self.vector_clock.lock().await;
518
519 if vector_clock.is_concurrent(&replicated_event.replication_metadata.vector_clock) {
520 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 async fn handle_conflict(&self, mut conflict_info: ConflictInfo) -> Result<()> {
549 match &self.config.conflict_resolution {
550 ConflictResolution::LastWriteWins => {
551 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 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 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 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 let mut queue = self.conflict_queue.lock().await;
625 queue.push_back(conflict_info);
626 }
627 }
628
629 Ok(())
630 }
631
632 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 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 for task in replication_tasks {
679 let _ = task.await;
680 }
681
682 Ok(())
683 }
684
685 async fn send_to_region(
687 _replicated_event: ReplicatedEvent,
688 _region_config: RegionConfig,
689 ) -> Result<()> {
690 time::sleep(Duration::from_millis(50)).await;
692
693 if fastrand::f32() < 0.05 {
695 return Err(anyhow!("Simulated network failure"));
697 }
698
699 Ok(())
700 }
701
702 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 async fn replicate_partitioned(
710 &self,
711 _replicated_event: &ReplicatedEvent,
712 _partition_strategy: &PartitionStrategy,
713 ) -> Result<()> {
714 Ok(())
717 }
718
719 async fn replicate_by_geography(
721 &self,
722 _replicated_event: &ReplicatedEvent,
723 _region_groups: &HashMap<String, Vec<String>>,
724 ) -> Result<()> {
725 Ok(())
728 }
729
730 async fn process_replicated_event(&self, _replicated_event: ReplicatedEvent) -> Result<()> {
732 Ok(())
735 }
736
737 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 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 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 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 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 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 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(®ion_id).await?;
828 }
829
830 Ok(())
831 }
832
833 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 let is_healthy = fastrand::f32() > 0.1; 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 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 #[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 #[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 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 #[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}