openigtlink_rust/io/
reconnect.rs

1//! Automatic reconnection configuration
2//!
3//! Provides reconnection strategy with exponential backoff for resilient connections.
4
5use std::time::Duration;
6
7/// Reconnection strategy configuration
8///
9/// Used with [`ClientBuilder`](crate::io::builder::ClientBuilder) to enable automatic
10/// reconnection after network failures.
11///
12/// # Examples
13///
14/// ```no_run
15/// use openigtlink_rust::io::builder::ClientBuilder;
16/// use openigtlink_rust::io::reconnect::ReconnectConfig;
17///
18/// # async fn example() -> Result<(), openigtlink_rust::error::IgtlError> {
19/// // Auto-reconnect with default settings (10 attempts)
20/// let config = ReconnectConfig::default();
21/// let client = ClientBuilder::new()
22///     .tcp("127.0.0.1:18944")
23///     .async_mode()
24///     .with_reconnect(config)
25///     .build()
26///     .await?;
27/// # Ok(())
28/// # }
29/// ```
30///
31/// ```no_run
32/// use openigtlink_rust::io::builder::ClientBuilder;
33/// use openigtlink_rust::io::reconnect::ReconnectConfig;
34///
35/// # async fn example() -> Result<(), openigtlink_rust::error::IgtlError> {
36/// // Infinite retries
37/// let config = ReconnectConfig::infinite();
38/// let client = ClientBuilder::new()
39///     .tcp("127.0.0.1:18944")
40///     .async_mode()
41///     .with_reconnect(config)
42///     .build()
43///     .await?;
44/// # Ok(())
45/// # }
46/// ```
47#[derive(Debug, Clone)]
48pub struct ReconnectConfig {
49    /// Maximum number of reconnection attempts (None = infinite)
50    pub max_attempts: Option<usize>,
51    /// Initial delay before first reconnection attempt
52    pub initial_delay: Duration,
53    /// Maximum delay between reconnection attempts
54    pub max_delay: Duration,
55    /// Backoff multiplier (delay is multiplied by this after each attempt)
56    pub backoff_multiplier: f64,
57    /// Whether to add random jitter to delays
58    pub use_jitter: bool,
59}
60
61impl Default for ReconnectConfig {
62    fn default() -> Self {
63        Self {
64            max_attempts: Some(10),
65            initial_delay: Duration::from_millis(100),
66            max_delay: Duration::from_secs(30),
67            backoff_multiplier: 2.0,
68            use_jitter: true,
69        }
70    }
71}
72
73impl ReconnectConfig {
74    /// Create config with infinite retries
75    ///
76    /// # Examples
77    ///
78    /// ```
79    /// use openigtlink_rust::io::reconnect::ReconnectConfig;
80    ///
81    /// let config = ReconnectConfig::infinite();
82    /// assert_eq!(config.max_attempts, None);
83    /// ```
84    pub fn infinite() -> Self {
85        Self {
86            max_attempts: None,
87            ..Default::default()
88        }
89    }
90
91    /// Create config with specific max attempts
92    ///
93    /// # Examples
94    ///
95    /// ```
96    /// use openigtlink_rust::io::reconnect::ReconnectConfig;
97    ///
98    /// let config = ReconnectConfig::with_max_attempts(5);
99    /// assert_eq!(config.max_attempts, Some(5));
100    /// ```
101    pub fn with_max_attempts(attempts: usize) -> Self {
102        Self {
103            max_attempts: Some(attempts),
104            ..Default::default()
105        }
106    }
107
108    /// Create config with custom delays
109    ///
110    /// # Examples
111    ///
112    /// ```
113    /// use openigtlink_rust::io::reconnect::ReconnectConfig;
114    /// use std::time::Duration;
115    ///
116    /// let config = ReconnectConfig::with_delays(
117    ///     Duration::from_millis(500),
118    ///     Duration::from_secs(60)
119    /// );
120    /// assert_eq!(config.initial_delay, Duration::from_millis(500));
121    /// assert_eq!(config.max_delay, Duration::from_secs(60));
122    /// ```
123    pub fn with_delays(initial: Duration, max: Duration) -> Self {
124        Self {
125            initial_delay: initial,
126            max_delay: max,
127            ..Default::default()
128        }
129    }
130
131    /// Calculate delay for a given attempt number
132    ///
133    /// Uses exponential backoff with optional jitter.
134    pub(crate) fn delay_for_attempt(&self, attempt: usize) -> Duration {
135        let delay_ms =
136            self.initial_delay.as_millis() as f64 * self.backoff_multiplier.powi(attempt as i32);
137
138        let mut delay =
139            Duration::from_millis(delay_ms.min(self.max_delay.as_millis() as f64) as u64);
140
141        // Add jitter if enabled (0-25% random variation)
142        if self.use_jitter {
143            use std::collections::hash_map::RandomState;
144            use std::hash::BuildHasher;
145
146            let hash = RandomState::new().hash_one(attempt);
147            let jitter = (hash % 25) as f64 / 100.0; // 0-25%
148
149            let jitter_ms = (delay.as_millis() as f64 * jitter) as u64;
150            delay = Duration::from_millis(delay.as_millis() as u64 + jitter_ms);
151        }
152
153        delay
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_reconnect_config_defaults() {
163        let config = ReconnectConfig::default();
164        assert_eq!(config.max_attempts, Some(10));
165        assert_eq!(config.initial_delay, Duration::from_millis(100));
166        assert_eq!(config.max_delay, Duration::from_secs(30));
167        assert_eq!(config.backoff_multiplier, 2.0);
168        assert!(config.use_jitter);
169    }
170
171    #[test]
172    fn test_reconnect_config_infinite() {
173        let config = ReconnectConfig::infinite();
174        assert_eq!(config.max_attempts, None);
175    }
176
177    #[test]
178    fn test_reconnect_config_delay_calculation() {
179        let config = ReconnectConfig {
180            initial_delay: Duration::from_millis(100),
181            max_delay: Duration::from_secs(10),
182            backoff_multiplier: 2.0,
183            use_jitter: false,
184            max_attempts: Some(10),
185        };
186
187        // Test exponential backoff
188        assert_eq!(config.delay_for_attempt(0), Duration::from_millis(100));
189        assert_eq!(config.delay_for_attempt(1), Duration::from_millis(200));
190        assert_eq!(config.delay_for_attempt(2), Duration::from_millis(400));
191        assert_eq!(config.delay_for_attempt(3), Duration::from_millis(800));
192    }
193}