Skip to main content

ipfrs_network/
peer_migration.rs

1//! Peer state migration for seamless node handoff in P2P networks.
2//!
3//! This module provides the infrastructure to migrate peer state (routing tables,
4//! provider records, connection metadata, etc.) from one node to another during
5//! planned handoffs or failover scenarios.
6//!
7//! # Overview
8//!
9//! When a node needs to leave the network gracefully (maintenance, scaling, etc.),
10//! its state must be transferred to one or more successor nodes to preserve
11//! network continuity. The `PeerMigrationManager` orchestrates this process
12//! through a structured state machine:
13//!
14//! ```text
15//! Idle -> Preparing -> Transferring -> Verifying -> Completed
16//!                 \         \              \
17//!                  `--------> Failed <------'
18//! ```
19//!
20//! # Key Features
21//!
22//! - **Chunked transfer**: Large state sets are broken into configurable chunks
23//! - **Checksum verification**: Integrity verification after transfer completion
24//! - **Concurrent migration limits**: Prevent resource exhaustion during bulk migrations
25//! - **Progress tracking**: Real-time progress reporting per migration
26//! - **Statistics**: Aggregate stats across all migrations for monitoring
27//! - **Cleanup**: Automatic pruning of completed migration records
28
29use std::collections::HashMap;
30
31/// State machine for a single peer migration operation.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum PeerMigrationState {
34    /// No migration activity.
35    Idle,
36    /// Gathering state and negotiating with target peer.
37    Preparing,
38    /// Actively transferring data items.
39    Transferring,
40    /// Transfer complete, verifying integrity via checksum.
41    Verifying,
42    /// Migration finished successfully.
43    Completed,
44    /// Migration failed with a reason.
45    Failed(String),
46}
47
48impl std::fmt::Display for PeerMigrationState {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        match self {
51            Self::Idle => write!(f, "Idle"),
52            Self::Preparing => write!(f, "Preparing"),
53            Self::Transferring => write!(f, "Transferring"),
54            Self::Verifying => write!(f, "Verifying"),
55            Self::Completed => write!(f, "Completed"),
56            Self::Failed(reason) => write!(f, "Failed({})", reason),
57        }
58    }
59}
60
61/// Record tracking a single migration between two peers.
62#[derive(Debug, Clone)]
63pub struct PeerMigrationRecord {
64    /// Unique identifier for this migration.
65    pub migration_id: u64,
66    /// Peer ID of the source (departing) node.
67    pub source_peer: String,
68    /// Peer ID of the target (receiving) node.
69    pub target_peer: String,
70    /// Current state of the migration.
71    pub state: PeerMigrationState,
72    /// Total number of items to transfer.
73    pub items_total: u64,
74    /// Number of items successfully transferred so far.
75    pub items_transferred: u64,
76    /// Total bytes transferred so far.
77    pub bytes_transferred: u64,
78    /// Timestamp (epoch ms) when the migration started.
79    pub started_at: u64,
80    /// Timestamp (epoch ms) when the migration completed (if finished).
81    pub completed_at: Option<u64>,
82    /// Checksum for integrity verification after transfer.
83    pub checksum: u64,
84}
85
86/// Configuration for the migration manager.
87#[derive(Debug, Clone)]
88pub struct PeerMigrationConfig {
89    /// Maximum number of concurrent active migrations.
90    pub max_concurrent: usize,
91    /// Number of items per transfer chunk.
92    pub chunk_size: usize,
93    /// Timeout in milliseconds for a single chunk transfer.
94    pub timeout_ms: u64,
95    /// Whether to verify checksums after transfer completion.
96    pub verify_after_transfer: bool,
97    /// Number of retry attempts for failed chunk transfers.
98    pub retry_count: u32,
99}
100
101impl Default for PeerMigrationConfig {
102    fn default() -> Self {
103        Self {
104            max_concurrent: 4,
105            chunk_size: 256,
106            timeout_ms: 30_000,
107            verify_after_transfer: true,
108            retry_count: 3,
109        }
110    }
111}
112
113/// A single item being migrated between peers.
114#[derive(Debug, Clone)]
115pub struct MigrationItem {
116    /// Key identifying this item (e.g., CID, routing key).
117    pub key: String,
118    /// Raw data payload.
119    pub data: Vec<u8>,
120    /// Sequence number for ordering within the migration.
121    pub sequence: u64,
122}
123
124/// Aggregate statistics across all migrations managed by a `PeerMigrationManager`.
125#[derive(Debug, Clone, Default)]
126pub struct PeerMigrationStats {
127    /// Total number of migrations ever started.
128    pub total_migrations: u64,
129    /// Number of migrations that completed successfully.
130    pub successful: u64,
131    /// Number of migrations that failed.
132    pub failed: u64,
133    /// Total bytes migrated across all successful migrations.
134    pub bytes_migrated: u64,
135    /// Average duration of completed migrations in milliseconds.
136    pub avg_duration_ms: f64,
137}
138
139/// Manages peer state migrations for seamless node handoff.
140pub struct PeerMigrationManager {
141    config: PeerMigrationConfig,
142    active_migrations: HashMap<u64, PeerMigrationRecord>,
143    completed_migrations: Vec<PeerMigrationRecord>,
144    next_id: u64,
145    stats: PeerMigrationStats,
146    /// Accumulated items per migration, keyed by migration_id.
147    items: HashMap<u64, Vec<MigrationItem>>,
148}
149
150impl PeerMigrationManager {
151    /// Create a new migration manager with the given configuration.
152    pub fn new(config: PeerMigrationConfig) -> Self {
153        Self {
154            config,
155            active_migrations: HashMap::new(),
156            completed_migrations: Vec::new(),
157            next_id: 1,
158            stats: PeerMigrationStats::default(),
159            items: HashMap::new(),
160        }
161    }
162
163    /// Start a new migration from `source` to `target`.
164    ///
165    /// Returns the migration ID on success, or an error if the concurrent
166    /// migration limit has been reached or the parameters are invalid.
167    pub fn start_migration(
168        &mut self,
169        source: &str,
170        target: &str,
171        total_items: u64,
172    ) -> Result<u64, String> {
173        if source.is_empty() {
174            return Err("source peer cannot be empty".to_string());
175        }
176        if target.is_empty() {
177            return Err("target peer cannot be empty".to_string());
178        }
179        if source == target {
180            return Err("source and target peers must be different".to_string());
181        }
182        if total_items == 0 {
183            return Err("total_items must be greater than zero".to_string());
184        }
185
186        let active = self.active_count();
187        if active >= self.config.max_concurrent {
188            return Err(format!(
189                "concurrent migration limit reached ({}/{})",
190                active, self.config.max_concurrent
191            ));
192        }
193
194        let id = self.next_id;
195        self.next_id = self.next_id.saturating_add(1);
196
197        let now = current_timestamp_ms();
198
199        let record = PeerMigrationRecord {
200            migration_id: id,
201            source_peer: source.to_string(),
202            target_peer: target.to_string(),
203            state: PeerMigrationState::Preparing,
204            items_total: total_items,
205            items_transferred: 0,
206            bytes_transferred: 0,
207            started_at: now,
208            completed_at: None,
209            checksum: 0,
210        };
211
212        self.active_migrations.insert(id, record);
213        self.items.insert(id, Vec::new());
214        self.stats.total_migrations = self.stats.total_migrations.saturating_add(1);
215
216        Ok(id)
217    }
218
219    /// Add items to an active migration.
220    ///
221    /// The migration must be in the `Transferring` state. Each item's bytes
222    /// are counted toward the transfer total.
223    pub fn add_items(
224        &mut self,
225        migration_id: u64,
226        new_items: Vec<MigrationItem>,
227    ) -> Result<(), String> {
228        let record = self
229            .active_migrations
230            .get_mut(&migration_id)
231            .ok_or_else(|| format!("migration {} not found", migration_id))?;
232
233        if record.state != PeerMigrationState::Transferring {
234            return Err(format!(
235                "migration {} is in state {}, expected Transferring",
236                migration_id, record.state
237            ));
238        }
239
240        let item_count = new_items.len() as u64;
241        let byte_count: u64 = new_items.iter().map(|item| item.data.len() as u64).sum();
242
243        // Prevent transferring more items than the declared total.
244        let new_transferred = record.items_transferred.saturating_add(item_count);
245        if new_transferred > record.items_total {
246            return Err(format!(
247                "adding {} items would exceed total ({} + {} > {})",
248                item_count, record.items_transferred, item_count, record.items_total
249            ));
250        }
251
252        record.items_transferred = new_transferred;
253        record.bytes_transferred = record.bytes_transferred.saturating_add(byte_count);
254
255        // Compute a simple additive checksum contribution from the items.
256        let checksum_delta: u64 = compute_items_checksum(&new_items);
257        record.checksum = record.checksum.wrapping_add(checksum_delta);
258
259        let stored = self.items.entry(migration_id).or_default();
260        stored.extend(new_items);
261
262        Ok(())
263    }
264
265    /// Advance the migration to its next state.
266    ///
267    /// State transitions follow:
268    /// `Preparing -> Transferring -> Verifying -> Completed`
269    ///
270    /// The `Verifying -> Completed` transition only succeeds if all items
271    /// have been transferred (when `verify_after_transfer` is enabled).
272    pub fn advance_state(&mut self, migration_id: u64) -> Result<PeerMigrationState, String> {
273        let verify = self.config.verify_after_transfer;
274
275        let record = self
276            .active_migrations
277            .get_mut(&migration_id)
278            .ok_or_else(|| format!("migration {} not found", migration_id))?;
279
280        let next = match &record.state {
281            PeerMigrationState::Idle => PeerMigrationState::Preparing,
282            PeerMigrationState::Preparing => PeerMigrationState::Transferring,
283            PeerMigrationState::Transferring => PeerMigrationState::Verifying,
284            PeerMigrationState::Verifying => {
285                // Verify all items were transferred if configured to do so.
286                if verify && record.items_transferred < record.items_total {
287                    return Err(format!(
288                        "verification failed: only {}/{} items transferred",
289                        record.items_transferred, record.items_total
290                    ));
291                }
292                PeerMigrationState::Completed
293            }
294            PeerMigrationState::Completed => {
295                return Err(format!("migration {} is already completed", migration_id));
296            }
297            PeerMigrationState::Failed(reason) => {
298                return Err(format!(
299                    "migration {} has failed ({}), cannot advance",
300                    migration_id, reason
301                ));
302            }
303        };
304
305        record.state = next.clone();
306
307        // If we just completed, finalize the record.
308        if next == PeerMigrationState::Completed {
309            self.finalize_migration(migration_id);
310        }
311
312        Ok(next)
313    }
314
315    /// Mark a migration as completed with a final checksum.
316    ///
317    /// The migration must be in the `Verifying` state. If `verify_after_transfer`
318    /// is enabled, the provided checksum is compared against the computed one.
319    pub fn complete_migration(&mut self, migration_id: u64, checksum: u64) -> Result<(), String> {
320        let verify = self.config.verify_after_transfer;
321
322        let record = self
323            .active_migrations
324            .get_mut(&migration_id)
325            .ok_or_else(|| format!("migration {} not found", migration_id))?;
326
327        if record.state != PeerMigrationState::Verifying {
328            return Err(format!(
329                "migration {} is in state {}, expected Verifying",
330                migration_id, record.state
331            ));
332        }
333
334        if verify && checksum != record.checksum {
335            return Err(format!(
336                "checksum mismatch: expected {}, got {}",
337                record.checksum, checksum
338            ));
339        }
340
341        record.state = PeerMigrationState::Completed;
342        record.checksum = checksum;
343
344        self.finalize_migration(migration_id);
345
346        Ok(())
347    }
348
349    /// Mark a migration as failed with the given reason.
350    pub fn fail_migration(&mut self, migration_id: u64, reason: &str) -> Result<(), String> {
351        let record = self
352            .active_migrations
353            .get_mut(&migration_id)
354            .ok_or_else(|| format!("migration {} not found", migration_id))?;
355
356        match &record.state {
357            PeerMigrationState::Completed => {
358                return Err(format!(
359                    "migration {} is already completed, cannot fail",
360                    migration_id
361                ));
362            }
363            PeerMigrationState::Failed(_) => {
364                return Err(format!("migration {} has already failed", migration_id));
365            }
366            _ => {}
367        }
368
369        record.state = PeerMigrationState::Failed(reason.to_string());
370        record.completed_at = Some(current_timestamp_ms());
371
372        self.stats.failed = self.stats.failed.saturating_add(1);
373
374        // Move to completed list.
375        if let Some(rec) = self.active_migrations.remove(&migration_id) {
376            self.completed_migrations.push(rec);
377        }
378        self.items.remove(&migration_id);
379
380        Ok(())
381    }
382
383    /// Get a reference to a migration record by ID.
384    ///
385    /// Searches both active and completed migrations.
386    pub fn get_migration(&self, migration_id: u64) -> Option<&PeerMigrationRecord> {
387        self.active_migrations.get(&migration_id).or_else(|| {
388            self.completed_migrations
389                .iter()
390                .find(|r| r.migration_id == migration_id)
391        })
392    }
393
394    /// Get the progress of a migration as a value between 0.0 and 1.0.
395    ///
396    /// Returns `None` if the migration does not exist.
397    pub fn progress(&self, migration_id: u64) -> Option<f64> {
398        self.get_migration(migration_id).map(|record| {
399            if record.items_total == 0 {
400                return 0.0;
401            }
402            let ratio = record.items_transferred as f64 / record.items_total as f64;
403            ratio.clamp(0.0, 1.0)
404        })
405    }
406
407    /// Return the number of currently active (non-completed, non-failed) migrations.
408    pub fn active_count(&self) -> usize {
409        self.active_migrations.len()
410    }
411
412    /// Remove completed/failed migrations with `completed_at` before the given timestamp.
413    ///
414    /// Returns the number of records removed.
415    pub fn cleanup_completed(&mut self, before: u64) -> usize {
416        let initial_len = self.completed_migrations.len();
417        self.completed_migrations
418            .retain(|rec| rec.completed_at.is_none_or(|ts| ts >= before));
419        initial_len - self.completed_migrations.len()
420    }
421
422    /// Get a reference to the aggregate migration statistics.
423    pub fn stats(&self) -> &PeerMigrationStats {
424        &self.stats
425    }
426
427    /// Find all migrations (active and completed) involving a given peer,
428    /// either as source or target.
429    pub fn migrations_for_peer(&self, peer: &str) -> Vec<&PeerMigrationRecord> {
430        let mut results: Vec<&PeerMigrationRecord> = Vec::new();
431
432        for record in self.active_migrations.values() {
433            if record.source_peer == peer || record.target_peer == peer {
434                results.push(record);
435            }
436        }
437
438        for record in &self.completed_migrations {
439            if record.source_peer == peer || record.target_peer == peer {
440                results.push(record);
441            }
442        }
443
444        results
445    }
446
447    /// Internal: move a completed migration from active to completed list and update stats.
448    fn finalize_migration(&mut self, migration_id: u64) {
449        let now = current_timestamp_ms();
450
451        if let Some(record) = self.active_migrations.get_mut(&migration_id) {
452            record.completed_at = Some(now);
453        }
454
455        if let Some(rec) = self.active_migrations.remove(&migration_id) {
456            let duration = now.saturating_sub(rec.started_at);
457            self.stats.successful = self.stats.successful.saturating_add(1);
458            self.stats.bytes_migrated = self
459                .stats
460                .bytes_migrated
461                .saturating_add(rec.bytes_transferred);
462
463            // Update rolling average duration.
464            let completed_count = self.stats.successful as f64;
465            if completed_count > 0.0 {
466                self.stats.avg_duration_ms = self.stats.avg_duration_ms
467                    + (duration as f64 - self.stats.avg_duration_ms) / completed_count;
468            }
469
470            self.completed_migrations.push(rec);
471        }
472
473        self.items.remove(&migration_id);
474    }
475
476    /// Get the configuration of this manager.
477    pub fn config(&self) -> &PeerMigrationConfig {
478        &self.config
479    }
480
481    /// Get the stored items for a migration (if still active).
482    pub fn get_items(&self, migration_id: u64) -> Option<&[MigrationItem]> {
483        self.items.get(&migration_id).map(|v| v.as_slice())
484    }
485}
486
487/// Compute a simple additive checksum over migration items.
488///
489/// Uses a combination of key bytes and data bytes with position weighting.
490fn compute_items_checksum(items: &[MigrationItem]) -> u64 {
491    let mut checksum: u64 = 0;
492    for item in items {
493        let mut item_hash: u64 = item.sequence;
494        for (i, b) in item.key.bytes().enumerate() {
495            item_hash = item_hash.wrapping_add((b as u64).wrapping_mul((i as u64).wrapping_add(1)));
496        }
497        for (i, b) in item.data.iter().enumerate() {
498            item_hash =
499                item_hash.wrapping_add((*b as u64).wrapping_mul((i as u64).wrapping_add(1)));
500        }
501        checksum = checksum.wrapping_add(item_hash);
502    }
503    checksum
504}
505
506/// Get current timestamp in milliseconds.
507///
508/// Falls back to 0 if the system clock is unavailable.
509fn current_timestamp_ms() -> u64 {
510    std::time::SystemTime::now()
511        .duration_since(std::time::UNIX_EPOCH)
512        .map(|d| d.as_millis() as u64)
513        .unwrap_or(0)
514}
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519
520    fn default_config() -> PeerMigrationConfig {
521        PeerMigrationConfig::default()
522    }
523
524    fn make_items(count: usize) -> Vec<MigrationItem> {
525        (0..count)
526            .map(|i| MigrationItem {
527                key: format!("key-{}", i),
528                data: vec![i as u8; 32],
529                sequence: i as u64,
530            })
531            .collect()
532    }
533
534    // ---------- Basic lifecycle ----------
535
536    #[test]
537    fn test_start_migration() {
538        let mut mgr = PeerMigrationManager::new(default_config());
539        let id = mgr
540            .start_migration("peer-a", "peer-b", 10)
541            .expect("should start");
542        assert_eq!(id, 1);
543        assert_eq!(mgr.active_count(), 1);
544    }
545
546    #[test]
547    fn test_full_lifecycle() {
548        let mut mgr = PeerMigrationManager::new(default_config());
549        let id = mgr.start_migration("peer-a", "peer-b", 3).expect("start");
550
551        // Preparing -> Transferring
552        let state = mgr.advance_state(id).expect("advance to transferring");
553        assert_eq!(state, PeerMigrationState::Transferring);
554
555        // Add items
556        mgr.add_items(id, make_items(3)).expect("add items");
557
558        // Transferring -> Verifying
559        let state = mgr.advance_state(id).expect("advance to verifying");
560        assert_eq!(state, PeerMigrationState::Verifying);
561
562        // Verifying -> Completed
563        let state = mgr.advance_state(id).expect("advance to completed");
564        assert_eq!(state, PeerMigrationState::Completed);
565
566        assert_eq!(mgr.active_count(), 0);
567        assert_eq!(mgr.stats().successful, 1);
568    }
569
570    #[test]
571    fn test_complete_with_checksum() {
572        let mut mgr = PeerMigrationManager::new(default_config());
573        let id = mgr.start_migration("peer-a", "peer-b", 2).expect("start");
574
575        mgr.advance_state(id).expect("to transferring");
576        let items = make_items(2);
577        let expected_checksum = compute_items_checksum(&items);
578        mgr.add_items(id, items).expect("add");
579        mgr.advance_state(id).expect("to verifying");
580
581        mgr.complete_migration(id, expected_checksum)
582            .expect("complete");
583
584        let rec = mgr.get_migration(id).expect("should find");
585        assert_eq!(rec.state, PeerMigrationState::Completed);
586    }
587
588    #[test]
589    fn test_checksum_mismatch() {
590        let mut mgr = PeerMigrationManager::new(default_config());
591        let id = mgr.start_migration("peer-a", "peer-b", 1).expect("start");
592        mgr.advance_state(id).expect("to transferring");
593        mgr.add_items(id, make_items(1)).expect("add");
594        mgr.advance_state(id).expect("to verifying");
595
596        let result = mgr.complete_migration(id, 999_999);
597        assert!(result.is_err());
598        assert!(result
599            .expect_err("should fail")
600            .contains("checksum mismatch"));
601    }
602
603    // ---------- State transitions ----------
604
605    #[test]
606    fn test_advance_from_idle() {
607        // Migrations start in Preparing, but test that Idle also advances.
608        let mut mgr = PeerMigrationManager::new(default_config());
609        let id = mgr.start_migration("a", "b", 1).expect("start");
610
611        // Force state to Idle to test transition from Idle.
612        mgr.active_migrations.get_mut(&id).expect("record").state = PeerMigrationState::Idle;
613        let state = mgr.advance_state(id).expect("advance");
614        assert_eq!(state, PeerMigrationState::Preparing);
615    }
616
617    #[test]
618    fn test_advance_completed_fails() {
619        let mut mgr = PeerMigrationManager::new(default_config());
620        let id = mgr.start_migration("a", "b", 1).expect("start");
621        mgr.advance_state(id).expect("to transferring");
622        mgr.add_items(id, make_items(1)).expect("add");
623        mgr.advance_state(id).expect("to verifying");
624        mgr.advance_state(id).expect("to completed");
625
626        // Migration is now in completed list, not active.
627        let result = mgr.advance_state(id);
628        assert!(result.is_err());
629    }
630
631    #[test]
632    fn test_advance_failed_fails() {
633        let mut mgr = PeerMigrationManager::new(default_config());
634        let id = mgr.start_migration("a", "b", 1).expect("start");
635        mgr.advance_state(id).expect("to transferring");
636
637        // Force to Failed.
638        mgr.active_migrations.get_mut(&id).expect("r").state =
639            PeerMigrationState::Failed("test".to_string());
640
641        let result = mgr.advance_state(id);
642        assert!(result.is_err());
643    }
644
645    #[test]
646    fn test_verification_fails_if_incomplete() {
647        let mut mgr = PeerMigrationManager::new(default_config());
648        let id = mgr.start_migration("a", "b", 5).expect("start");
649        mgr.advance_state(id).expect("to transferring");
650        mgr.add_items(id, make_items(2)).expect("add partial");
651        mgr.advance_state(id).expect("to verifying");
652
653        // Try to advance from Verifying -> Completed with incomplete items.
654        let result = mgr.advance_state(id);
655        assert!(result.is_err());
656        assert!(result.expect_err("err").contains("verification failed"));
657    }
658
659    #[test]
660    fn test_verification_skipped_when_disabled() {
661        let config = PeerMigrationConfig {
662            verify_after_transfer: false,
663            ..default_config()
664        };
665        let mut mgr = PeerMigrationManager::new(config);
666        let id = mgr.start_migration("a", "b", 10).expect("start");
667        mgr.advance_state(id).expect("to transferring");
668        mgr.add_items(id, make_items(2)).expect("add partial");
669        mgr.advance_state(id).expect("to verifying");
670
671        // Should succeed even with incomplete items.
672        let state = mgr.advance_state(id).expect("to completed");
673        assert_eq!(state, PeerMigrationState::Completed);
674    }
675
676    // ---------- Concurrent migration limit ----------
677
678    #[test]
679    fn test_concurrent_limit() {
680        let config = PeerMigrationConfig {
681            max_concurrent: 2,
682            ..default_config()
683        };
684        let mut mgr = PeerMigrationManager::new(config);
685
686        mgr.start_migration("a", "b", 1).expect("first");
687        mgr.start_migration("c", "d", 1).expect("second");
688
689        let result = mgr.start_migration("e", "f", 1);
690        assert!(result.is_err());
691        assert!(result.expect_err("err").contains("limit reached"));
692    }
693
694    #[test]
695    fn test_concurrent_limit_freed_after_completion() {
696        let config = PeerMigrationConfig {
697            max_concurrent: 1,
698            verify_after_transfer: false,
699            ..default_config()
700        };
701        let mut mgr = PeerMigrationManager::new(config);
702
703        let id1 = mgr.start_migration("a", "b", 1).expect("first");
704
705        // Cannot start another.
706        assert!(mgr.start_migration("c", "d", 1).is_err());
707
708        // Complete the first.
709        mgr.advance_state(id1).expect("to transferring");
710        mgr.advance_state(id1).expect("to verifying");
711        mgr.advance_state(id1).expect("to completed");
712
713        // Now we can start another.
714        mgr.start_migration("c", "d", 1)
715            .expect("second should work");
716    }
717
718    // ---------- Progress tracking ----------
719
720    #[test]
721    fn test_progress_zero() {
722        let mut mgr = PeerMigrationManager::new(default_config());
723        let id = mgr.start_migration("a", "b", 10).expect("start");
724        assert_eq!(mgr.progress(id), Some(0.0));
725    }
726
727    #[test]
728    fn test_progress_partial() {
729        let mut mgr = PeerMigrationManager::new(default_config());
730        let id = mgr.start_migration("a", "b", 4).expect("start");
731        mgr.advance_state(id).expect("to transferring");
732        mgr.add_items(id, make_items(2)).expect("add");
733
734        let p = mgr.progress(id).expect("progress");
735        assert!((p - 0.5).abs() < 1e-9);
736    }
737
738    #[test]
739    fn test_progress_complete() {
740        let mut mgr = PeerMigrationManager::new(default_config());
741        let id = mgr.start_migration("a", "b", 3).expect("start");
742        mgr.advance_state(id).expect("to transferring");
743        mgr.add_items(id, make_items(3)).expect("add all");
744
745        let p = mgr.progress(id).expect("progress");
746        assert!((p - 1.0).abs() < 1e-9);
747    }
748
749    #[test]
750    fn test_progress_nonexistent() {
751        let mgr = PeerMigrationManager::new(default_config());
752        assert_eq!(mgr.progress(999), None);
753    }
754
755    // ---------- Failure ----------
756
757    #[test]
758    fn test_fail_migration() {
759        let mut mgr = PeerMigrationManager::new(default_config());
760        let id = mgr.start_migration("a", "b", 5).expect("start");
761        mgr.advance_state(id).expect("to transferring");
762
763        mgr.fail_migration(id, "network timeout").expect("fail");
764
765        let rec = mgr.get_migration(id).expect("find");
766        assert_eq!(
767            rec.state,
768            PeerMigrationState::Failed("network timeout".to_string())
769        );
770        assert_eq!(mgr.active_count(), 0);
771        assert_eq!(mgr.stats().failed, 1);
772    }
773
774    #[test]
775    fn test_fail_already_completed() {
776        let config = PeerMigrationConfig {
777            verify_after_transfer: false,
778            ..default_config()
779        };
780        let mut mgr = PeerMigrationManager::new(config);
781        let id = mgr.start_migration("a", "b", 1).expect("start");
782        mgr.advance_state(id).expect("to transferring");
783        mgr.advance_state(id).expect("to verifying");
784        mgr.advance_state(id).expect("to completed");
785
786        // Cannot fail a completed migration (it's in completed_migrations now).
787        let result = mgr.fail_migration(id, "too late");
788        assert!(result.is_err());
789    }
790
791    #[test]
792    fn test_fail_already_failed() {
793        let mut mgr = PeerMigrationManager::new(default_config());
794        let id = mgr.start_migration("a", "b", 1).expect("start");
795        mgr.fail_migration(id, "first").expect("fail once");
796
797        let result = mgr.fail_migration(id, "second");
798        assert!(result.is_err());
799    }
800
801    // ---------- Cleanup ----------
802
803    #[test]
804    fn test_cleanup_completed() {
805        let config = PeerMigrationConfig {
806            verify_after_transfer: false,
807            ..default_config()
808        };
809        let mut mgr = PeerMigrationManager::new(config);
810
811        // Create and complete two migrations.
812        let id1 = mgr.start_migration("a", "b", 1).expect("m1");
813        mgr.advance_state(id1).expect("t");
814        mgr.advance_state(id1).expect("v");
815        mgr.advance_state(id1).expect("c");
816
817        let id2 = mgr.start_migration("c", "d", 1).expect("m2");
818        mgr.advance_state(id2).expect("t");
819        mgr.advance_state(id2).expect("v");
820        mgr.advance_state(id2).expect("c");
821
822        // Cleanup with a far-future timestamp should remove both.
823        let removed = mgr.cleanup_completed(u64::MAX);
824        assert_eq!(removed, 2);
825        assert!(mgr.completed_migrations.is_empty());
826    }
827
828    #[test]
829    fn test_cleanup_none_removed() {
830        let mut mgr = PeerMigrationManager::new(default_config());
831        let removed = mgr.cleanup_completed(0);
832        assert_eq!(removed, 0);
833    }
834
835    // ---------- Stats ----------
836
837    #[test]
838    fn test_stats_initial() {
839        let mgr = PeerMigrationManager::new(default_config());
840        let s = mgr.stats();
841        assert_eq!(s.total_migrations, 0);
842        assert_eq!(s.successful, 0);
843        assert_eq!(s.failed, 0);
844        assert_eq!(s.bytes_migrated, 0);
845        assert!((s.avg_duration_ms - 0.0).abs() < 1e-9);
846    }
847
848    #[test]
849    fn test_stats_after_success() {
850        let config = PeerMigrationConfig {
851            verify_after_transfer: false,
852            ..default_config()
853        };
854        let mut mgr = PeerMigrationManager::new(config);
855        let id = mgr.start_migration("a", "b", 2).expect("start");
856        mgr.advance_state(id).expect("t");
857        mgr.add_items(id, make_items(2)).expect("add");
858        mgr.advance_state(id).expect("v");
859        mgr.advance_state(id).expect("c");
860
861        let s = mgr.stats();
862        assert_eq!(s.total_migrations, 1);
863        assert_eq!(s.successful, 1);
864        assert!(s.bytes_migrated > 0);
865    }
866
867    #[test]
868    fn test_stats_after_failure() {
869        let mut mgr = PeerMigrationManager::new(default_config());
870        let id = mgr.start_migration("a", "b", 1).expect("start");
871        mgr.fail_migration(id, "oops").expect("fail");
872
873        let s = mgr.stats();
874        assert_eq!(s.total_migrations, 1);
875        assert_eq!(s.failed, 1);
876        assert_eq!(s.successful, 0);
877    }
878
879    // ---------- Item transfer accounting ----------
880
881    #[test]
882    fn test_add_items_wrong_state() {
883        let mut mgr = PeerMigrationManager::new(default_config());
884        let id = mgr.start_migration("a", "b", 1).expect("start");
885
886        // Still in Preparing state.
887        let result = mgr.add_items(id, make_items(1));
888        assert!(result.is_err());
889    }
890
891    #[test]
892    fn test_add_items_exceeds_total() {
893        let mut mgr = PeerMigrationManager::new(default_config());
894        let id = mgr.start_migration("a", "b", 2).expect("start");
895        mgr.advance_state(id).expect("to transferring");
896
897        let result = mgr.add_items(id, make_items(3));
898        assert!(result.is_err());
899        assert!(result.expect_err("err").contains("exceed total"));
900    }
901
902    #[test]
903    fn test_add_items_bytes_tracked() {
904        let mut mgr = PeerMigrationManager::new(default_config());
905        let id = mgr.start_migration("a", "b", 5).expect("start");
906        mgr.advance_state(id).expect("to transferring");
907
908        let items = vec![
909            MigrationItem {
910                key: "k1".to_string(),
911                data: vec![0u8; 100],
912                sequence: 0,
913            },
914            MigrationItem {
915                key: "k2".to_string(),
916                data: vec![0u8; 200],
917                sequence: 1,
918            },
919        ];
920        mgr.add_items(id, items).expect("add");
921
922        let rec = mgr.get_migration(id).expect("find");
923        assert_eq!(rec.items_transferred, 2);
924        assert_eq!(rec.bytes_transferred, 300);
925    }
926
927    #[test]
928    fn test_add_items_nonexistent_migration() {
929        let mut mgr = PeerMigrationManager::new(default_config());
930        let result = mgr.add_items(999, make_items(1));
931        assert!(result.is_err());
932    }
933
934    // ---------- migrations_for_peer ----------
935
936    #[test]
937    fn test_migrations_for_peer_source() {
938        let mut mgr = PeerMigrationManager::new(default_config());
939        mgr.start_migration("peer-x", "peer-y", 1).expect("m1");
940        mgr.start_migration("peer-z", "peer-w", 1).expect("m2");
941
942        let results = mgr.migrations_for_peer("peer-x");
943        assert_eq!(results.len(), 1);
944        assert_eq!(results[0].source_peer, "peer-x");
945    }
946
947    #[test]
948    fn test_migrations_for_peer_target() {
949        let mut mgr = PeerMigrationManager::new(default_config());
950        mgr.start_migration("peer-a", "peer-b", 1).expect("m1");
951
952        let results = mgr.migrations_for_peer("peer-b");
953        assert_eq!(results.len(), 1);
954        assert_eq!(results[0].target_peer, "peer-b");
955    }
956
957    #[test]
958    fn test_migrations_for_peer_empty() {
959        let mgr = PeerMigrationManager::new(default_config());
960        let results = mgr.migrations_for_peer("nobody");
961        assert!(results.is_empty());
962    }
963
964    // ---------- Error cases ----------
965
966    #[test]
967    fn test_start_empty_source() {
968        let mut mgr = PeerMigrationManager::new(default_config());
969        let result = mgr.start_migration("", "b", 1);
970        assert!(result.is_err());
971    }
972
973    #[test]
974    fn test_start_empty_target() {
975        let mut mgr = PeerMigrationManager::new(default_config());
976        let result = mgr.start_migration("a", "", 1);
977        assert!(result.is_err());
978    }
979
980    #[test]
981    fn test_start_same_source_target() {
982        let mut mgr = PeerMigrationManager::new(default_config());
983        let result = mgr.start_migration("a", "a", 1);
984        assert!(result.is_err());
985    }
986
987    #[test]
988    fn test_start_zero_items() {
989        let mut mgr = PeerMigrationManager::new(default_config());
990        let result = mgr.start_migration("a", "b", 0);
991        assert!(result.is_err());
992    }
993
994    #[test]
995    fn test_complete_wrong_state() {
996        let mut mgr = PeerMigrationManager::new(default_config());
997        let id = mgr.start_migration("a", "b", 1).expect("start");
998
999        // Still Preparing, not Verifying.
1000        let result = mgr.complete_migration(id, 0);
1001        assert!(result.is_err());
1002    }
1003
1004    #[test]
1005    fn test_complete_nonexistent() {
1006        let mut mgr = PeerMigrationManager::new(default_config());
1007        let result = mgr.complete_migration(999, 0);
1008        assert!(result.is_err());
1009    }
1010
1011    // ---------- Config and misc ----------
1012
1013    #[test]
1014    fn test_config_access() {
1015        let config = PeerMigrationConfig {
1016            max_concurrent: 8,
1017            chunk_size: 512,
1018            timeout_ms: 60_000,
1019            verify_after_transfer: false,
1020            retry_count: 5,
1021        };
1022        let mgr = PeerMigrationManager::new(config);
1023        assert_eq!(mgr.config().max_concurrent, 8);
1024        assert_eq!(mgr.config().chunk_size, 512);
1025    }
1026
1027    #[test]
1028    fn test_default_config() {
1029        let config = PeerMigrationConfig::default();
1030        assert_eq!(config.max_concurrent, 4);
1031        assert!(config.verify_after_transfer);
1032    }
1033
1034    #[test]
1035    fn test_state_display() {
1036        assert_eq!(format!("{}", PeerMigrationState::Idle), "Idle");
1037        assert_eq!(format!("{}", PeerMigrationState::Preparing), "Preparing");
1038        assert_eq!(
1039            format!("{}", PeerMigrationState::Transferring),
1040            "Transferring"
1041        );
1042        assert_eq!(format!("{}", PeerMigrationState::Verifying), "Verifying");
1043        assert_eq!(format!("{}", PeerMigrationState::Completed), "Completed");
1044        assert_eq!(
1045            format!("{}", PeerMigrationState::Failed("err".to_string())),
1046            "Failed(err)"
1047        );
1048    }
1049
1050    #[test]
1051    fn test_migration_id_increments() {
1052        let mut mgr = PeerMigrationManager::new(default_config());
1053        let id1 = mgr.start_migration("a", "b", 1).expect("m1");
1054        let id2 = mgr.start_migration("c", "d", 1).expect("m2");
1055        assert_eq!(id2, id1 + 1);
1056    }
1057
1058    #[test]
1059    fn test_get_items() {
1060        let mut mgr = PeerMigrationManager::new(default_config());
1061        let id = mgr.start_migration("a", "b", 3).expect("start");
1062        mgr.advance_state(id).expect("to transferring");
1063
1064        let items = make_items(2);
1065        mgr.add_items(id, items).expect("add");
1066
1067        let stored = mgr.get_items(id).expect("get");
1068        assert_eq!(stored.len(), 2);
1069        assert_eq!(stored[0].key, "key-0");
1070        assert_eq!(stored[1].key, "key-1");
1071    }
1072
1073    #[test]
1074    fn test_checksum_computation_deterministic() {
1075        let items = make_items(5);
1076        let c1 = compute_items_checksum(&items);
1077        let c2 = compute_items_checksum(&items);
1078        assert_eq!(c1, c2);
1079    }
1080
1081    #[test]
1082    fn test_checksum_different_for_different_items() {
1083        let items_a = make_items(3);
1084        let items_b = vec![MigrationItem {
1085            key: "different".to_string(),
1086            data: vec![255; 64],
1087            sequence: 99,
1088        }];
1089        let c1 = compute_items_checksum(&items_a);
1090        let c2 = compute_items_checksum(&items_b);
1091        assert_ne!(c1, c2);
1092    }
1093}