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