Skip to main content

mssql_client/config/
types.rs

1//! Supporting configuration types for redirect handling, timeouts, and retry policies.
2
3use std::time::Duration;
4
5/// Configuration for Azure SQL redirect handling.
6///
7/// Azure SQL Gateway may redirect connections to different backend servers.
8/// This configuration controls how the driver handles these redirects.
9#[derive(Debug, Clone)]
10pub struct RedirectConfig {
11    /// Maximum number of redirect attempts (default: 2).
12    pub max_redirects: u8,
13    /// Whether to follow redirects automatically (default: true).
14    pub follow_redirects: bool,
15}
16
17impl Default for RedirectConfig {
18    fn default() -> Self {
19        Self {
20            max_redirects: 2,
21            follow_redirects: true,
22        }
23    }
24}
25
26impl RedirectConfig {
27    /// Create a new redirect configuration with defaults.
28    #[must_use]
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    /// Set the maximum number of redirect attempts.
34    #[must_use]
35    pub fn max_redirects(mut self, max: u8) -> Self {
36        self.max_redirects = max;
37        self
38    }
39
40    /// Enable or disable automatic redirect following.
41    #[must_use]
42    pub fn follow_redirects(mut self, follow: bool) -> Self {
43        self.follow_redirects = follow;
44        self
45    }
46
47    /// Disable automatic redirect following.
48    ///
49    /// When disabled, the driver will return an error with the redirect
50    /// information instead of automatically following the redirect.
51    #[must_use]
52    pub fn no_follow() -> Self {
53        Self {
54            max_redirects: 0,
55            follow_redirects: false,
56        }
57    }
58}
59
60/// Timeout configuration for various connection phases.
61///
62/// Per ARCHITECTURE.md ยง4.4, different phases of connection and command
63/// execution have separate timeout controls.
64#[derive(Debug, Clone)]
65pub struct TimeoutConfig {
66    /// Time to establish TCP connection (default: 15s).
67    pub connect_timeout: Duration,
68    /// Time to complete TLS handshake (default: 10s).
69    pub tls_timeout: Duration,
70    /// Time to complete login sequence (default: 30s).
71    pub login_timeout: Duration,
72    /// Default timeout for command execution (default: 30s).
73    pub command_timeout: Duration,
74    /// Time before idle connection is closed (default: 300s).
75    pub idle_timeout: Duration,
76    /// Interval for connection keep-alive (default: 30s).
77    pub keepalive_interval: Option<Duration>,
78}
79
80impl Default for TimeoutConfig {
81    fn default() -> Self {
82        Self {
83            connect_timeout: Duration::from_secs(15),
84            tls_timeout: Duration::from_secs(10),
85            login_timeout: Duration::from_secs(30),
86            command_timeout: Duration::from_secs(30),
87            idle_timeout: Duration::from_secs(300),
88            keepalive_interval: Some(Duration::from_secs(30)),
89        }
90    }
91}
92
93impl TimeoutConfig {
94    /// Create a new timeout configuration with defaults.
95    #[must_use]
96    pub fn new() -> Self {
97        Self::default()
98    }
99
100    /// Set the TCP connection timeout.
101    #[must_use]
102    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
103        self.connect_timeout = timeout;
104        self
105    }
106
107    /// Set the TLS handshake timeout.
108    #[must_use]
109    pub fn tls_timeout(mut self, timeout: Duration) -> Self {
110        self.tls_timeout = timeout;
111        self
112    }
113
114    /// Set the login sequence timeout.
115    #[must_use]
116    pub fn login_timeout(mut self, timeout: Duration) -> Self {
117        self.login_timeout = timeout;
118        self
119    }
120
121    /// Set the default command execution timeout.
122    #[must_use]
123    pub fn command_timeout(mut self, timeout: Duration) -> Self {
124        self.command_timeout = timeout;
125        self
126    }
127
128    /// Set the idle connection timeout.
129    #[must_use]
130    pub fn idle_timeout(mut self, timeout: Duration) -> Self {
131        self.idle_timeout = timeout;
132        self
133    }
134
135    /// Set the keep-alive interval.
136    #[must_use]
137    pub fn keepalive_interval(mut self, interval: Option<Duration>) -> Self {
138        self.keepalive_interval = interval;
139        self
140    }
141
142    /// Disable keep-alive.
143    #[must_use]
144    pub fn no_keepalive(mut self) -> Self {
145        self.keepalive_interval = None;
146        self
147    }
148
149    /// Get the total time allowed for a full connection (TCP + TLS + login).
150    #[must_use]
151    pub fn total_connect_timeout(&self) -> Duration {
152        self.connect_timeout + self.tls_timeout + self.login_timeout
153    }
154}
155
156/// Retry policy for transient error handling.
157///
158/// Per ADR-009, the driver can automatically retry operations that fail
159/// with transient errors (deadlocks, Azure service busy, etc.).
160#[derive(Debug, Clone)]
161pub struct RetryPolicy {
162    /// Maximum number of retry attempts (default: 3).
163    pub max_retries: u32,
164    /// Initial backoff duration before first retry (default: 100ms).
165    pub initial_backoff: Duration,
166    /// Maximum backoff duration between retries (default: 30s).
167    pub max_backoff: Duration,
168    /// Multiplier for exponential backoff (default: 2.0).
169    pub backoff_multiplier: f64,
170    /// Whether to add random jitter to backoff times (default: true).
171    pub jitter: bool,
172}
173
174impl Default for RetryPolicy {
175    fn default() -> Self {
176        Self {
177            max_retries: 3,
178            initial_backoff: Duration::from_millis(100),
179            max_backoff: Duration::from_secs(30),
180            backoff_multiplier: 2.0,
181            jitter: true,
182        }
183    }
184}
185
186impl RetryPolicy {
187    /// Create a new retry policy with defaults.
188    #[must_use]
189    pub fn new() -> Self {
190        Self::default()
191    }
192
193    /// Set the maximum number of retry attempts.
194    #[must_use]
195    pub fn max_retries(mut self, max: u32) -> Self {
196        self.max_retries = max;
197        self
198    }
199
200    /// Set the initial backoff duration.
201    #[must_use]
202    pub fn initial_backoff(mut self, backoff: Duration) -> Self {
203        self.initial_backoff = backoff;
204        self
205    }
206
207    /// Set the maximum backoff duration.
208    #[must_use]
209    pub fn max_backoff(mut self, backoff: Duration) -> Self {
210        self.max_backoff = backoff;
211        self
212    }
213
214    /// Set the backoff multiplier for exponential backoff.
215    #[must_use]
216    pub fn backoff_multiplier(mut self, multiplier: f64) -> Self {
217        self.backoff_multiplier = multiplier;
218        self
219    }
220
221    /// Enable or disable jitter.
222    #[must_use]
223    pub fn jitter(mut self, enabled: bool) -> Self {
224        self.jitter = enabled;
225        self
226    }
227
228    /// Disable automatic retries.
229    #[must_use]
230    pub fn no_retry() -> Self {
231        Self {
232            max_retries: 0,
233            ..Self::default()
234        }
235    }
236
237    /// Calculate the backoff duration for a given retry attempt.
238    ///
239    /// Uses exponential backoff with optional jitter.
240    #[must_use]
241    pub fn backoff_for_attempt(&self, attempt: u32) -> Duration {
242        if attempt == 0 {
243            return Duration::ZERO;
244        }
245
246        let base = self.initial_backoff.as_millis() as f64
247            * self
248                .backoff_multiplier
249                .powi(attempt.saturating_sub(1) as i32);
250        let capped = base.min(self.max_backoff.as_millis() as f64);
251
252        if self.jitter {
253            // Simple jitter: multiply by random factor between 0.5 and 1.5
254            // In production, this would use a proper RNG
255            Duration::from_millis(capped as u64)
256        } else {
257            Duration::from_millis(capped as u64)
258        }
259    }
260
261    /// Check if more retries are allowed for the given attempt number.
262    #[must_use]
263    pub fn should_retry(&self, attempt: u32) -> bool {
264        attempt < self.max_retries
265    }
266}