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}