use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use tracing::{debug, info, warn};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ReplicationPolicy {
None,
Explicit {
regions: Vec<String>,
},
Count {
count: usize,
},
All,
Topology {
key: String,
count_per_key: usize,
},
}
impl Default for ReplicationPolicy {
fn default() -> Self {
Self::None
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplicationStatus {
pub resource_id: String,
pub primary_region: String,
pub replica_regions: Vec<String>,
pub region_states: HashMap<String, ReplicaState>,
pub last_sync: chrono::DateTime<chrono::Utc>,
pub lag_ms: HashMap<String, u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReplicaState {
InSync,
Syncing,
Lagging,
Failed,
Creating,
Deleting,
}
pub struct ReplicationController {
status: Arc<RwLock<HashMap<String, ReplicationStatus>>>,
default_policy: RwLock<ReplicationPolicy>,
regions: RwLock<HashSet<String>>,
callbacks: RwLock<Vec<Arc<dyn ReplicationCallback + Send + Sync>>>,
}
pub trait ReplicationCallback: Send + Sync {
fn on_replicate(&self, resource_id: &str, source: &str, target: &str);
fn on_sync_complete(&self, resource_id: &str, region: &str);
fn on_sync_failed(&self, resource_id: &str, region: &str, error: &str);
}
impl ReplicationController {
pub fn new() -> Self {
Self {
status: Arc::new(RwLock::new(HashMap::new())),
default_policy: RwLock::new(ReplicationPolicy::None),
regions: RwLock::new(HashSet::new()),
callbacks: RwLock::new(Vec::new()),
}
}
pub fn set_default_policy(&self, policy: ReplicationPolicy) {
*self.default_policy.write() = policy;
}
pub fn register_region(&self, region: impl Into<String>) {
self.regions.write().insert(region.into());
}
pub fn unregister_region(&self, region: &str) {
self.regions.write().remove(region);
}
pub fn add_callback<C: ReplicationCallback + 'static>(&self, callback: C) {
self.callbacks.write().push(Arc::new(callback));
}
pub fn replicate(&self, resource_id: impl Into<String>, primary_region: impl Into<String>, policy: Option<ReplicationPolicy>) {
let resource_id = resource_id.into();
let primary_region = primary_region.into();
let policy = policy.unwrap_or_else(|| self.default_policy.read().clone());
let target_regions = self.resolve_target_regions(&primary_region, &policy);
if target_regions.is_empty() {
debug!(resource_id = %resource_id, "No replication targets");
return;
}
info!(
resource_id = %resource_id,
primary = %primary_region,
targets = ?target_regions,
"Starting replication"
);
let mut region_states = HashMap::new();
region_states.insert(primary_region.clone(), ReplicaState::InSync);
for region in &target_regions {
region_states.insert(region.clone(), ReplicaState::Creating);
}
let status = ReplicationStatus {
resource_id: resource_id.clone(),
primary_region: primary_region.clone(),
replica_regions: target_regions.clone(),
region_states,
last_sync: chrono::Utc::now(),
lag_ms: HashMap::new(),
};
self.status.write().insert(resource_id.clone(), status);
let callbacks = self.callbacks.read();
for region in &target_regions {
for callback in callbacks.iter() {
callback.on_replicate(&resource_id, &primary_region, region);
}
}
}
fn resolve_target_regions(&self, primary: &str, policy: &ReplicationPolicy) -> Vec<String> {
let regions = self.regions.read();
let available: Vec<_> = regions.iter()
.filter(|r| *r != primary)
.cloned()
.collect();
match policy {
ReplicationPolicy::None => Vec::new(),
ReplicationPolicy::Explicit { regions: targets } => {
targets.iter()
.filter(|r| available.contains(r))
.cloned()
.collect()
}
ReplicationPolicy::Count { count } => {
available.into_iter().take(*count).collect()
}
ReplicationPolicy::All => available,
ReplicationPolicy::Topology { key, count_per_key } => {
available.into_iter().take(*count_per_key).collect()
}
}
}
pub fn update_state(&self, resource_id: &str, region: &str, state: ReplicaState) {
let mut statuses = self.status.write();
if let Some(status) = statuses.get_mut(resource_id) {
let old_state = status.region_states.get(region).copied();
status.region_states.insert(region.to_string(), state);
if old_state != Some(state) {
let callbacks = self.callbacks.read();
match state {
ReplicaState::InSync => {
for callback in callbacks.iter() {
callback.on_sync_complete(resource_id, region);
}
}
ReplicaState::Failed => {
for callback in callbacks.iter() {
callback.on_sync_failed(resource_id, region, "Replication failed");
}
}
_ => {}
}
}
}
}
pub fn update_lag(&self, resource_id: &str, region: &str, lag_ms: u64) {
let mut statuses = self.status.write();
if let Some(status) = statuses.get_mut(resource_id) {
status.lag_ms.insert(region.to_string(), lag_ms);
status.last_sync = chrono::Utc::now();
let state = if lag_ms == 0 {
ReplicaState::InSync
} else if lag_ms < 1000 {
ReplicaState::Syncing
} else {
ReplicaState::Lagging
};
status.region_states.insert(region.to_string(), state);
}
}
pub fn get_status(&self, resource_id: &str) -> Option<ReplicationStatus> {
self.status.read().get(resource_id).cloned()
}
pub fn list_replicated(&self) -> Vec<ReplicationStatus> {
self.status.read().values().cloned().collect()
}
pub fn stop_replication(&self, resource_id: &str) {
if let Some(mut status) = self.status.write().remove(resource_id) {
info!(resource_id = %resource_id, "Stopping replication");
for (region, state) in status.region_states.iter_mut() {
if region != &status.primary_region {
*state = ReplicaState::Deleting;
}
}
}
}
pub fn promote(&self, resource_id: &str, new_primary: &str) -> bool {
let mut statuses = self.status.write();
if let Some(status) = statuses.get_mut(resource_id) {
if !status.replica_regions.contains(&new_primary.to_string())
&& status.primary_region != new_primary {
warn!(
resource_id = %resource_id,
new_primary = %new_primary,
"Cannot promote: region is not a replica"
);
return false;
}
let old_primary = status.primary_region.clone();
status.primary_region = new_primary.to_string();
status.replica_regions.retain(|r| r != new_primary);
if !status.replica_regions.contains(&old_primary) {
status.replica_regions.push(old_primary);
}
info!(
resource_id = %resource_id,
old_primary = %status.replica_regions.last().unwrap_or(&String::new()),
new_primary = %new_primary,
"Promoted replica to primary"
);
return true;
}
false
}
pub fn is_replicated_to(&self, resource_id: &str, region: &str) -> bool {
self.status.read()
.get(resource_id)
.map(|s| s.primary_region == region || s.replica_regions.contains(®ion.to_string()))
.unwrap_or(false)
}
pub fn healthy_replicas(&self, resource_id: &str) -> Vec<String> {
self.status.read()
.get(resource_id)
.map(|s| {
let mut healthy = vec![s.primary_region.clone()];
healthy.extend(
s.region_states.iter()
.filter(|(r, state)| {
*r != &s.primary_region && **state == ReplicaState::InSync
})
.map(|(r, _)| r.clone())
);
healthy
})
.unwrap_or_default()
}
}
impl Default for ReplicationController {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_replication_policy() {
let controller = ReplicationController::new();
controller.register_region("us-east-1");
controller.register_region("us-west-2");
controller.register_region("eu-west-1");
controller.replicate(
"resource-1",
"us-east-1",
Some(ReplicationPolicy::Count { count: 2 })
);
let status = controller.get_status("resource-1").unwrap();
assert_eq!(status.primary_region, "us-east-1");
assert_eq!(status.replica_regions.len(), 2);
}
#[test]
fn test_replica_promotion() {
let controller = ReplicationController::new();
controller.register_region("us-east-1");
controller.register_region("eu-west-1");
controller.replicate(
"resource-1",
"us-east-1",
Some(ReplicationPolicy::All)
);
controller.update_state("resource-1", "eu-west-1", ReplicaState::InSync);
assert!(controller.promote("resource-1", "eu-west-1"));
let status = controller.get_status("resource-1").unwrap();
assert_eq!(status.primary_region, "eu-west-1");
assert!(status.replica_regions.contains(&"us-east-1".to_string()));
}
#[test]
fn test_healthy_replicas() {
let controller = ReplicationController::new();
controller.register_region("us-east-1");
controller.register_region("us-west-2");
controller.register_region("eu-west-1");
controller.replicate(
"resource-1",
"us-east-1",
Some(ReplicationPolicy::All)
);
controller.update_state("resource-1", "us-west-2", ReplicaState::InSync);
controller.update_state("resource-1", "eu-west-1", ReplicaState::Failed);
let healthy = controller.healthy_replicas("resource-1");
assert!(healthy.contains(&"us-east-1".to_string()));
assert!(healthy.contains(&"us-west-2".to_string()));
assert!(!healthy.contains(&"eu-west-1".to_string()));
}
}