1use crate::traits::BlockStore;
39use ipfrs_core::{Cid, Error, Result};
40use std::collections::{HashMap, HashSet};
41use std::sync::Arc;
42use std::time::{Duration, Instant};
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum SyncStrategy {
47 Full,
49 Incremental,
51 Bidirectional,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum ConflictStrategy {
58 KeepSource,
60 KeepTarget,
62 KeepNewer,
64 Fail,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct SyncResult {
71 pub blocks_synced: usize,
73 pub bytes_synced: u64,
75 pub conflicts: usize,
77 pub duration: Duration,
79 pub conflicting_cids: Vec<Cid>,
81}
82
83#[derive(Debug, Clone, Default)]
85pub struct ReplicationState {
86 pub last_sync: Option<Instant>,
88 pub last_synced_cids: HashSet<Cid>,
90 pub total_blocks_synced: usize,
92 pub total_bytes_synced: u64,
94}
95
96pub struct Replicator<S: BlockStore, T: BlockStore> {
98 source: Arc<S>,
100 target: Arc<T>,
102 state: parking_lot::RwLock<ReplicationState>,
104}
105
106impl<S: BlockStore, T: BlockStore> Replicator<S, T> {
107 pub fn new(source: Arc<S>, target: Arc<T>) -> Self {
109 Self {
110 source,
111 target,
112 state: parking_lot::RwLock::new(ReplicationState::default()),
113 }
114 }
115
116 pub async fn sync(
122 &self,
123 strategy: SyncStrategy,
124 conflict_strategy: Option<ConflictStrategy>,
125 ) -> Result<SyncResult> {
126 let start_time = Instant::now();
127 let conflict_strategy = conflict_strategy.unwrap_or(ConflictStrategy::KeepSource);
128
129 match strategy {
130 SyncStrategy::Full => self.sync_full(conflict_strategy).await,
131 SyncStrategy::Incremental => self.sync_incremental(conflict_strategy).await,
132 SyncStrategy::Bidirectional => {
133 let result1 = self.sync_incremental(conflict_strategy).await?;
135
136 let reverse = Replicator::new(self.target.clone(), self.source.clone());
138 let result2 = reverse.sync_incremental(conflict_strategy).await?;
139
140 Ok(SyncResult {
141 blocks_synced: result1.blocks_synced + result2.blocks_synced,
142 bytes_synced: result1.bytes_synced + result2.bytes_synced,
143 conflicts: result1.conflicts + result2.conflicts,
144 duration: start_time.elapsed(),
145 conflicting_cids: [result1.conflicting_cids, result2.conflicting_cids].concat(),
146 })
147 }
148 }
149 }
150
151 async fn sync_full(&self, conflict_strategy: ConflictStrategy) -> Result<SyncResult> {
153 let start_time = Instant::now();
154
155 let source_cids = self.source.list_cids()?;
157
158 self.sync_cids(&source_cids, conflict_strategy, start_time)
159 .await
160 }
161
162 async fn sync_incremental(&self, conflict_strategy: ConflictStrategy) -> Result<SyncResult> {
164 let start_time = Instant::now();
165
166 let source_cids = self.source.list_cids()?;
168
169 let target_has = self.target.has_many(&source_cids).await?;
171 let missing_cids: Vec<Cid> = source_cids
172 .into_iter()
173 .zip(target_has.iter())
174 .filter_map(|(cid, has)| if !*has { Some(cid) } else { None })
175 .collect();
176
177 self.sync_cids(&missing_cids, conflict_strategy, start_time)
178 .await
179 }
180
181 async fn sync_cids(
183 &self,
184 cids: &[Cid],
185 conflict_strategy: ConflictStrategy,
186 start_time: Instant,
187 ) -> Result<SyncResult> {
188 let mut blocks_synced = 0;
189 let mut bytes_synced = 0u64;
190 let mut conflicts = 0;
191 let mut conflicting_cids = Vec::new();
192 let mut synced_cids = HashSet::new();
193
194 const BATCH_SIZE: usize = 100;
196 for chunk in cids.chunks(BATCH_SIZE) {
197 let blocks = self.source.get_many(chunk).await?;
199
200 let mut blocks_to_put = Vec::new();
201
202 for (cid, block_opt) in chunk.iter().zip(blocks.iter()) {
203 if let Some(block) = block_opt {
204 if let Some(existing) = self.target.get(cid).await? {
206 let should_replace = match conflict_strategy {
208 ConflictStrategy::KeepSource => true,
209 ConflictStrategy::KeepTarget => false,
210 ConflictStrategy::KeepNewer => {
211 block.data().len() > existing.data().len()
214 }
215 ConflictStrategy::Fail => {
216 return Err(Error::Storage(format!(
217 "Conflict detected for block {cid}"
218 )));
219 }
220 };
221
222 if should_replace {
223 blocks_to_put.push(block.clone());
224 bytes_synced += block.data().len() as u64;
225 synced_cids.insert(*cid);
226 }
227
228 conflicts += 1;
229 conflicting_cids.push(*cid);
230 } else {
231 blocks_to_put.push(block.clone());
233 bytes_synced += block.data().len() as u64;
234 synced_cids.insert(*cid);
235 }
236 }
237 }
238
239 if !blocks_to_put.is_empty() {
241 self.target.put_many(&blocks_to_put).await?;
242 blocks_synced += blocks_to_put.len();
243 }
244 }
245
246 {
248 let mut state = self.state.write();
249 state.last_sync = Some(Instant::now());
250 state.last_synced_cids = synced_cids;
251 state.total_blocks_synced += blocks_synced;
252 state.total_bytes_synced += bytes_synced;
253 }
254
255 Ok(SyncResult {
256 blocks_synced,
257 bytes_synced,
258 conflicts,
259 duration: start_time.elapsed(),
260 conflicting_cids,
261 })
262 }
263
264 pub fn state(&self) -> ReplicationState {
266 self.state.read().clone()
267 }
268
269 pub async fn sync_blocks(
271 &self,
272 cids: &[Cid],
273 conflict_strategy: Option<ConflictStrategy>,
274 ) -> Result<SyncResult> {
275 let conflict_strategy = conflict_strategy.unwrap_or(ConflictStrategy::KeepSource);
276 self.sync_cids(cids, conflict_strategy, Instant::now())
277 .await
278 }
279
280 pub async fn verify(&self) -> Result<Vec<Cid>> {
282 let source_cids = self.source.list_cids()?;
283 let target_has = self.target.has_many(&source_cids).await?;
284
285 let missing: Vec<Cid> = source_cids
286 .into_iter()
287 .zip(target_has.iter())
288 .filter_map(|(cid, has)| if !*has { Some(cid) } else { None })
289 .collect();
290
291 Ok(missing)
292 }
293}
294
295pub struct ReplicationManager<S: BlockStore> {
297 primary: Arc<S>,
299 replicas: Vec<Arc<S>>,
301 stats: parking_lot::RwLock<HashMap<usize, ReplicationState>>,
303}
304
305impl<S: BlockStore> ReplicationManager<S> {
306 pub fn new(primary: Arc<S>) -> Self {
308 Self {
309 primary,
310 replicas: Vec::new(),
311 stats: parking_lot::RwLock::new(HashMap::new()),
312 }
313 }
314
315 pub fn add_replica(&mut self, replica: Arc<S>) {
317 self.replicas.push(replica);
318 }
319
320 pub async fn sync_all(&self, strategy: SyncStrategy) -> Result<Vec<SyncResult>> {
322 let mut results = Vec::new();
323
324 for (idx, replica) in self.replicas.iter().enumerate() {
325 let replicator = Replicator::new(self.primary.clone(), replica.clone());
326 let result = replicator.sync(strategy, None).await?;
327
328 self.stats.write().insert(idx, replicator.state());
330
331 results.push(result);
332 }
333
334 Ok(results)
335 }
336
337 pub fn replica_stats(&self, index: usize) -> Option<ReplicationState> {
339 self.stats.read().get(&index).cloned()
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346 use crate::blockstore::{BlockStoreConfig, SledBlockStore};
347 use bytes::Bytes;
348 use ipfrs_core::Block;
349
350 #[tokio::test]
351 async fn test_full_sync() {
352 let source_config = BlockStoreConfig {
353 path: std::env::temp_dir().join("ipfrs-replication-source"),
354 cache_size: 10 * 1024 * 1024,
355 };
356 let _ = std::fs::remove_dir_all(&source_config.path);
357
358 let target_config = BlockStoreConfig {
359 path: std::env::temp_dir().join("ipfrs-replication-target"),
360 cache_size: 10 * 1024 * 1024,
361 };
362 let _ = std::fs::remove_dir_all(&target_config.path);
363
364 let source =
365 Arc::new(SledBlockStore::new(source_config).expect("source store should be created"));
366 let target =
367 Arc::new(SledBlockStore::new(target_config).expect("target store should be created"));
368
369 let block1 = Block::new(Bytes::from("block 1")).expect("block 1 should be created");
371 let block2 = Block::new(Bytes::from("block 2")).expect("block 2 should be created");
372 source
373 .put(&block1)
374 .await
375 .expect("block 1 should be put in source");
376 source
377 .put(&block2)
378 .await
379 .expect("block 2 should be put in source");
380
381 let replicator = Replicator::new(source.clone(), target.clone());
383 let result = replicator
384 .sync(SyncStrategy::Full, None)
385 .await
386 .expect("full sync should succeed");
387
388 assert_eq!(result.blocks_synced, 2);
389 assert_eq!(result.conflicts, 0);
390 assert!(target
391 .has(block1.cid())
392 .await
393 .expect("target should have block 1"));
394 assert!(target
395 .has(block2.cid())
396 .await
397 .expect("target should have block 2"));
398 }
399
400 #[tokio::test]
401 async fn test_incremental_sync() {
402 let source_config = BlockStoreConfig {
403 path: std::env::temp_dir().join("ipfrs-replication-inc-source"),
404 cache_size: 10 * 1024 * 1024,
405 };
406 let _ = std::fs::remove_dir_all(&source_config.path);
407
408 let target_config = BlockStoreConfig {
409 path: std::env::temp_dir().join("ipfrs-replication-inc-target"),
410 cache_size: 10 * 1024 * 1024,
411 };
412 let _ = std::fs::remove_dir_all(&target_config.path);
413
414 let source = Arc::new(
415 SledBlockStore::new(source_config).expect("test: source store should be created"),
416 );
417 let target = Arc::new(
418 SledBlockStore::new(target_config).expect("test: target store should be created"),
419 );
420
421 let block1 = Block::new(Bytes::from("block 1")).expect("test: block 1 should be created");
423 source
424 .put(&block1)
425 .await
426 .expect("test: block 1 should be put in source");
427 target
428 .put(&block1)
429 .await
430 .expect("test: block 1 should be put in target");
431
432 let block2 = Block::new(Bytes::from("block 2")).expect("test: block 2 should be created");
434 source
435 .put(&block2)
436 .await
437 .expect("test: block 2 should be put in source");
438
439 let replicator = Replicator::new(source.clone(), target.clone());
441 let result = replicator
442 .sync(SyncStrategy::Incremental, None)
443 .await
444 .expect("test: incremental sync should succeed");
445
446 assert_eq!(result.blocks_synced, 1);
447 assert!(target
448 .has(block2.cid())
449 .await
450 .expect("test: target should have block 2"));
451 }
452
453 #[tokio::test]
454 async fn test_conflict_resolution() {
455 let source_config = BlockStoreConfig {
456 path: std::env::temp_dir().join("ipfrs-replication-conflict-source"),
457 cache_size: 10 * 1024 * 1024,
458 };
459 let _ = std::fs::remove_dir_all(&source_config.path);
460
461 let target_config = BlockStoreConfig {
462 path: std::env::temp_dir().join("ipfrs-replication-conflict-target"),
463 cache_size: 10 * 1024 * 1024,
464 };
465 let _ = std::fs::remove_dir_all(&target_config.path);
466
467 let source = Arc::new(
468 SledBlockStore::new(source_config)
469 .expect("test: conflict source store should be created"),
470 );
471 let target = Arc::new(
472 SledBlockStore::new(target_config)
473 .expect("test: conflict target store should be created"),
474 );
475
476 let block1 = Block::new(Bytes::from("source version"))
478 .expect("test: source version block should be created");
479 source
480 .put(&block1)
481 .await
482 .expect("test: source version block should be put in source");
483
484 let replicator = Replicator::new(source.clone(), target.clone());
487 let result = replicator
488 .sync(SyncStrategy::Full, Some(ConflictStrategy::KeepSource))
489 .await
490 .expect("test: conflict resolution sync should succeed");
491
492 assert!(result.blocks_synced > 0);
493 }
494}