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