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}