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}