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    ///
91    /// Applied to every command path — `query`, `execute`, named-parameter
92    /// and multi-result variants, stored-procedure calls, and bulk-insert
93    /// network phases. On expiry the driver sends an Attention packet to
94    /// cancel the command, drains the acknowledgement (bounded at 5 s), and
95    /// returns [`Error::CommandTimeout`](crate::Error::CommandTimeout) with
96    /// the connection left usable. Exceptions that abandon the connection
97    /// instead of cancelling: a server that never acknowledges the
98    /// attention, and a timeout during the bulk-load data transfer (an
99    /// Attention cannot be interleaved into a partially-sent BulkLoad
100    /// message). Set to `Duration::ZERO` for no limit (matching ADO.NET's
101    /// `CommandTimeout = 0`).
102    pub command_timeout: Duration,
103    /// Time before idle connection is closed (default: 300s).
104    pub idle_timeout: Duration,
105    /// Interval for connection keep-alive (default: 30s).
106    pub keepalive_interval: Option<Duration>,
107}
108
109impl Default for TimeoutConfig {
110    fn default() -> Self {
111        Self {
112            connect_timeout: Duration::from_secs(15),
113            tls_timeout: Duration::from_secs(10),
114            login_timeout: Duration::from_secs(30),
115            command_timeout: Duration::from_secs(30),
116            idle_timeout: Duration::from_secs(300),
117            keepalive_interval: Some(Duration::from_secs(30)),
118        }
119    }
120}
121
122impl TimeoutConfig {
123    /// Create a new timeout configuration with defaults.
124    #[must_use]
125    pub fn new() -> Self {
126        Self::default()
127    }
128
129    /// Set the TCP connection timeout.
130    #[must_use]
131    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
132        self.connect_timeout = timeout;
133        self
134    }
135
136    /// Set the TLS handshake timeout.
137    #[must_use]
138    pub fn tls_timeout(mut self, timeout: Duration) -> Self {
139        self.tls_timeout = timeout;
140        self
141    }
142
143    /// Set the login sequence timeout.
144    #[must_use]
145    pub fn login_timeout(mut self, timeout: Duration) -> Self {
146        self.login_timeout = timeout;
147        self
148    }
149
150    /// Set the default command execution timeout.
151    #[must_use]
152    pub fn command_timeout(mut self, timeout: Duration) -> Self {
153        self.command_timeout = timeout;
154        self
155    }
156
157    /// Set the idle connection timeout.
158    #[must_use]
159    pub fn idle_timeout(mut self, timeout: Duration) -> Self {
160        self.idle_timeout = timeout;
161        self
162    }
163
164    /// Set the keep-alive interval.
165    #[must_use]
166    pub fn keepalive_interval(mut self, interval: Option<Duration>) -> Self {
167        self.keepalive_interval = interval;
168        self
169    }
170
171    /// Disable keep-alive.
172    #[must_use]
173    pub fn no_keepalive(mut self) -> Self {
174        self.keepalive_interval = None;
175        self
176    }
177
178    /// Get the total time allowed for a full connection (TCP + TLS + login).
179    #[must_use]
180    pub fn total_connect_timeout(&self) -> Duration {
181        self.connect_timeout + self.tls_timeout + self.login_timeout
182    }
183}
184
185/// Retry policy for transient error handling.
186///
187/// Per ADR-009, the driver can automatically retry operations that fail
188/// with transient errors (deadlocks, Azure service busy, etc.).
189#[derive(Debug, Clone)]
190pub struct RetryPolicy {
191    /// Maximum number of retry attempts (default: 3).
192    pub max_retries: u32,
193    /// Initial backoff duration before first retry (default: 100ms).
194    pub initial_backoff: Duration,
195    /// Maximum backoff duration between retries (default: 30s).
196    pub max_backoff: Duration,
197    /// Multiplier for exponential backoff (default: 2.0).
198    pub backoff_multiplier: f64,
199    /// Whether to add random jitter to backoff times (default: true).
200    pub jitter: bool,
201}
202
203impl Default for RetryPolicy {
204    fn default() -> Self {
205        Self {
206            max_retries: 3,
207            initial_backoff: Duration::from_millis(100),
208            max_backoff: Duration::from_secs(30),
209            backoff_multiplier: 2.0,
210            jitter: true,
211        }
212    }
213}
214
215impl RetryPolicy {
216    /// Create a new retry policy with defaults.
217    #[must_use]
218    pub fn new() -> Self {
219        Self::default()
220    }
221
222    /// Set the maximum number of retry attempts.
223    #[must_use]
224    pub fn max_retries(mut self, max: u32) -> Self {
225        self.max_retries = max;
226        self
227    }
228
229    /// Set the initial backoff duration.
230    #[must_use]
231    pub fn initial_backoff(mut self, backoff: Duration) -> Self {
232        self.initial_backoff = backoff;
233        self
234    }
235
236    /// Set the maximum backoff duration.
237    #[must_use]
238    pub fn max_backoff(mut self, backoff: Duration) -> Self {
239        self.max_backoff = backoff;
240        self
241    }
242
243    /// Set the backoff multiplier for exponential backoff.
244    #[must_use]
245    pub fn backoff_multiplier(mut self, multiplier: f64) -> Self {
246        self.backoff_multiplier = multiplier;
247        self
248    }
249
250    /// Enable or disable jitter.
251    #[must_use]
252    pub fn jitter(mut self, enabled: bool) -> Self {
253        self.jitter = enabled;
254        self
255    }
256
257    /// Disable automatic retries.
258    #[must_use]
259    pub fn no_retry() -> Self {
260        Self {
261            max_retries: 0,
262            ..Self::default()
263        }
264    }
265
266    /// Calculate the backoff duration for a given retry attempt.
267    ///
268    /// Uses exponential backoff with optional jitter.
269    #[must_use]
270    pub fn backoff_for_attempt(&self, attempt: u32) -> Duration {
271        if attempt == 0 {
272            return Duration::ZERO;
273        }
274
275        let base = self.initial_backoff.as_millis() as f64
276            * self
277                .backoff_multiplier
278                .powi(attempt.saturating_sub(1) as i32);
279        let capped = base.min(self.max_backoff.as_millis() as f64);
280
281        if self.jitter {
282            // Simple jitter: multiply by random factor between 0.5 and 1.5
283            // In production, this would use a proper RNG
284            Duration::from_millis(capped as u64)
285        } else {
286            Duration::from_millis(capped as u64)
287        }
288    }
289
290    /// Check if more retries are allowed for the given attempt number.
291    #[must_use]
292    pub fn should_retry(&self, attempt: u32) -> bool {
293        attempt < self.max_retries
294    }
295}