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