forge_orchestration/federation/
replication.rs1use std::collections::{HashMap, HashSet};
9use std::sync::Arc;
10use parking_lot::RwLock;
11use serde::{Deserialize, Serialize};
12use tracing::{debug, info, warn};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub enum ReplicationPolicy {
17 None,
19 Explicit {
21 regions: Vec<String>,
23 },
24 Count {
26 count: usize,
28 },
29 All,
31 Topology {
33 key: String,
35 count_per_key: usize,
37 },
38}
39
40impl Default for ReplicationPolicy {
41 fn default() -> Self {
42 Self::None
43 }
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct ReplicationStatus {
49 pub resource_id: String,
51 pub primary_region: String,
53 pub replica_regions: Vec<String>,
55 pub region_states: HashMap<String, ReplicaState>,
57 pub last_sync: chrono::DateTime<chrono::Utc>,
59 pub lag_ms: HashMap<String, u64>,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65pub enum ReplicaState {
66 InSync,
68 Syncing,
70 Lagging,
72 Failed,
74 Creating,
76 Deleting,
78}
79
80pub struct ReplicationController {
82 status: Arc<RwLock<HashMap<String, ReplicationStatus>>>,
84 default_policy: RwLock<ReplicationPolicy>,
86 regions: RwLock<HashSet<String>>,
88 callbacks: RwLock<Vec<Arc<dyn ReplicationCallback + Send + Sync>>>,
90}
91
92pub trait ReplicationCallback: Send + Sync {
94 fn on_replicate(&self, resource_id: &str, source: &str, target: &str);
96
97 fn on_sync_complete(&self, resource_id: &str, region: &str);
99
100 fn on_sync_failed(&self, resource_id: &str, region: &str, error: &str);
102}
103
104impl ReplicationController {
105 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 pub fn set_default_policy(&self, policy: ReplicationPolicy) {
117 *self.default_policy.write() = policy;
118 }
119
120 pub fn register_region(&self, region: impl Into<String>) {
122 self.regions.write().insert(region.into());
123 }
124
125 pub fn unregister_region(&self, region: &str) {
127 self.regions.write().remove(region);
128 }
129
130 pub fn add_callback<C: ReplicationCallback + 'static>(&self, callback: C) {
132 self.callbacks.write().push(Arc::new(callback));
133 }
134
135 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 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 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 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 available.into_iter().take(*count_per_key).collect()
206 }
207 }
208 }
209
210 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 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 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 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 pub fn get_status(&self, resource_id: &str) -> Option<ReplicationStatus> {
262 self.status.read().get(resource_id).cloned()
263 }
264
265 pub fn list_replicated(&self) -> Vec<ReplicationStatus> {
267 self.status.read().values().cloned().collect()
268 }
269
270 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 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 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 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 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(®ion.to_string()))
326 .unwrap_or(false)
327 }
328
329 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 controller.update_state("resource-1", "eu-west-1", ReplicaState::InSync);
392
393 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}