1#[cfg(not(target_arch = "wasm32"))]
4use dapi_grpc::tonic::transport::Certificate;
5use std::time::Duration;
6
7const DEFAULT_CONNECT_TIMEOUT: Option<Duration> = None;
9const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
10const DEFAULT_RETRIES: usize = 5;
11const DEFAULT_BAN_FAILED_ADDRESS: bool = true;
12
13#[derive(Debug, Clone, Copy, Default)]
21pub struct RequestSettings {
22 pub connect_timeout: Option<Duration>,
24 pub timeout: Option<Duration>,
37 pub retries: Option<usize>,
40 pub ban_failed_address: Option<bool>,
42 pub max_decoding_message_size: Option<usize>,
44}
45
46impl RequestSettings {
47 pub const fn default() -> Self {
50 RequestSettings {
51 connect_timeout: None,
52 timeout: None,
53 retries: None,
54 ban_failed_address: None,
55 max_decoding_message_size: None,
56 }
57 }
58
59 pub fn override_by(self, rhs: RequestSettings) -> Self {
63 RequestSettings {
64 connect_timeout: rhs.connect_timeout.or(self.connect_timeout),
65 timeout: rhs.timeout.or(self.timeout),
66 retries: rhs.retries.or(self.retries),
67 ban_failed_address: rhs.ban_failed_address.or(self.ban_failed_address),
68 max_decoding_message_size: rhs
69 .max_decoding_message_size
70 .or(self.max_decoding_message_size),
71 }
72 }
73
74 pub fn finalize(self) -> AppliedRequestSettings {
76 AppliedRequestSettings {
77 connect_timeout: self.connect_timeout.or(DEFAULT_CONNECT_TIMEOUT),
78 timeout: self.timeout.unwrap_or(DEFAULT_TIMEOUT),
79 retries: self.retries.unwrap_or(DEFAULT_RETRIES),
80 ban_failed_address: self
81 .ban_failed_address
82 .unwrap_or(DEFAULT_BAN_FAILED_ADDRESS),
83 max_decoding_message_size: self.max_decoding_message_size,
84 #[cfg(not(target_arch = "wasm32"))]
85 ca_certificate: None,
86 }
87 }
88}
89
90#[derive(Debug, Clone)]
96pub struct AppliedRequestSettings {
97 pub connect_timeout: Option<Duration>,
99 pub timeout: Duration,
101 pub retries: usize,
103 pub ban_failed_address: bool,
105 pub max_decoding_message_size: Option<usize>,
107 #[cfg(not(target_arch = "wasm32"))]
109 pub ca_certificate: Option<Certificate>,
110}
111impl AppliedRequestSettings {
112 #[cfg(not(target_arch = "wasm32"))]
116 pub fn with_ca_certificate(mut self, ca_cert: Option<Certificate>) -> Self {
117 self.ca_certificate = ca_cert;
118 self
119 }
120
121 pub fn attempt_deadline(&self) -> Option<Duration> {
128 if self.timeout.is_zero() {
129 return None;
130 }
131 Some(
132 self.timeout
133 .saturating_add(self.connect_timeout.unwrap_or_default()),
134 )
135 }
136
137 pub(crate) fn connection_key(&self) -> String {
144 let Self {
147 #[cfg(not(target_arch = "wasm32"))]
148 connect_timeout,
149 #[cfg(target_arch = "wasm32")]
150 connect_timeout: _,
151 timeout: _,
152 retries: _,
153 ban_failed_address: _,
154 max_decoding_message_size,
155 #[cfg(not(target_arch = "wasm32"))]
156 ca_certificate,
157 } = self;
158
159 #[cfg(target_arch = "wasm32")]
163 let connect_timeout = None::<Duration>;
164
165 #[cfg(not(target_arch = "wasm32"))]
169 let ca_certificate = ca_certificate.as_ref().map(|cert| {
170 use std::fmt::Write;
171 let bytes = cert.as_ref();
172 let mut hex = String::with_capacity(bytes.len() * 2);
173 for byte in bytes {
174 write!(hex, "{byte:02x}").expect("writing to a String cannot fail");
175 }
176 hex
177 });
178 #[cfg(target_arch = "wasm32")]
179 let ca_certificate: Option<String> = None;
180
181 format!(
182 "connect_timeout={:?},max_decoding_message_size={:?},ca_certificate={:?}",
183 connect_timeout, max_decoding_message_size, ca_certificate
184 )
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn test_request_settings_override_by() {
194 let base = RequestSettings {
195 timeout: Some(Duration::from_secs(5)),
196 retries: Some(3),
197 connect_timeout: Some(Duration::from_secs(2)),
198 ban_failed_address: Some(true),
199 max_decoding_message_size: Some(1024),
200 };
201
202 let override_settings = RequestSettings {
204 timeout: Some(Duration::from_secs(10)),
205 retries: None,
206 connect_timeout: None,
207 ban_failed_address: None,
208 max_decoding_message_size: None,
209 };
210
211 let result = base.override_by(override_settings);
212 assert_eq!(result.timeout, Some(Duration::from_secs(10))); assert_eq!(result.retries, Some(3)); assert_eq!(result.connect_timeout, Some(Duration::from_secs(2))); assert_eq!(result.ban_failed_address, Some(true)); assert_eq!(result.max_decoding_message_size, Some(1024)); }
218
219 #[test]
220 fn test_request_settings_override_by_empty() {
221 let base = RequestSettings {
222 timeout: Some(Duration::from_secs(5)),
223 retries: Some(3),
224 connect_timeout: None,
225 ban_failed_address: None,
226 max_decoding_message_size: None,
227 };
228
229 let result = base.override_by(RequestSettings::default());
230 assert_eq!(result.timeout, Some(Duration::from_secs(5)));
231 assert_eq!(result.retries, Some(3));
232 }
233
234 #[test]
235 fn test_request_settings_finalize_defaults() {
236 let settings = RequestSettings::default();
237 let applied = settings.finalize();
238
239 assert_eq!(applied.connect_timeout, None);
240 assert_eq!(applied.timeout, Duration::from_secs(10));
241 assert_eq!(applied.retries, 5);
242 assert!(applied.ban_failed_address);
243 assert!(applied.max_decoding_message_size.is_none());
244 }
245
246 #[test]
247 fn test_request_settings_finalize_custom() {
248 let settings = RequestSettings {
249 connect_timeout: Some(Duration::from_secs(3)),
250 timeout: Some(Duration::from_secs(30)),
251 retries: Some(10),
252 ban_failed_address: Some(false),
253 max_decoding_message_size: Some(4096),
254 };
255
256 let applied = settings.finalize();
257 assert_eq!(applied.connect_timeout, Some(Duration::from_secs(3)));
258 assert_eq!(applied.timeout, Duration::from_secs(30));
259 assert_eq!(applied.retries, 10);
260 assert!(!applied.ban_failed_address);
261 assert_eq!(applied.max_decoding_message_size, Some(4096));
262 }
263
264 #[cfg(not(target_arch = "wasm32"))]
265 #[test]
266 fn test_applied_settings_with_ca_certificate_none() {
267 let applied = RequestSettings::default().finalize();
268 let result = applied.with_ca_certificate(None);
269 assert!(result.ca_certificate.is_none());
270 }
271
272 #[cfg(not(target_arch = "wasm32"))]
273 #[test]
274 fn test_applied_settings_with_ca_certificate_some() {
275 let applied = RequestSettings::default().finalize();
276 let cert = Certificate::from_pem("fake-pem-data");
277 let result = applied.with_ca_certificate(Some(cert));
278 assert!(result.ca_certificate.is_some());
279 }
280
281 #[test]
282 fn should_bound_attempt_by_timeout_plus_connect_timeout() {
283 let applied = RequestSettings {
284 timeout: Some(Duration::from_secs(10)),
285 connect_timeout: Some(Duration::from_secs(3)),
286 ..RequestSettings::default()
287 }
288 .finalize();
289 assert_eq!(applied.attempt_deadline(), Some(Duration::from_secs(13)));
290
291 let default = RequestSettings::default().finalize();
292 assert_eq!(default.attempt_deadline(), Some(Duration::from_secs(10)));
293 }
294
295 #[test]
296 fn should_not_bound_attempt_when_timeout_is_zero() {
297 let applied = RequestSettings {
298 timeout: Some(Duration::ZERO),
299 connect_timeout: Some(Duration::from_secs(3)),
300 ..RequestSettings::default()
301 }
302 .finalize();
303 assert_eq!(applied.attempt_deadline(), None);
304 }
305
306 #[test]
307 fn should_saturate_attempt_deadline_instead_of_overflowing() {
308 let applied = RequestSettings {
309 timeout: Some(Duration::MAX),
310 connect_timeout: Some(Duration::from_secs(1)),
311 ..RequestSettings::default()
312 }
313 .finalize();
314 assert_eq!(applied.attempt_deadline(), Some(Duration::MAX));
315 }
316
317 #[test]
318 fn test_connection_key_ignores_per_request_settings() {
319 let custom = RequestSettings {
320 timeout: Some(Duration::from_secs(30)),
321 retries: Some(1),
322 ban_failed_address: Some(false),
323 ..RequestSettings::default()
324 }
325 .finalize();
326 let default = RequestSettings::default().finalize();
327
328 assert_eq!(
329 custom.connection_key(),
330 default.connection_key(),
331 "timeout/retries/banning must not split pooled connections"
332 );
333 }
334
335 #[test]
336 fn test_connection_key_differs_on_connection_settings() {
337 let default = RequestSettings::default().finalize();
338
339 let connect_timeout = RequestSettings {
340 connect_timeout: Some(Duration::from_secs(3)),
341 ..RequestSettings::default()
342 }
343 .finalize();
344 assert_ne!(default.connection_key(), connect_timeout.connection_key());
345
346 let decode_limit = RequestSettings {
347 max_decoding_message_size: Some(16 * 1024 * 1024),
348 ..RequestSettings::default()
349 }
350 .finalize();
351 assert_ne!(default.connection_key(), decode_limit.connection_key());
352 }
353
354 #[cfg(not(target_arch = "wasm32"))]
355 #[test]
356 fn test_connection_key_differs_on_ca_certificate() {
357 let default = RequestSettings::default().finalize();
358 let with_ca = RequestSettings::default()
359 .finalize()
360 .with_ca_certificate(Some(Certificate::from_pem("fake-pem-data")));
361
362 assert_ne!(default.connection_key(), with_ca.connection_key());
363
364 let with_other_ca = RequestSettings::default()
365 .finalize()
366 .with_ca_certificate(Some(Certificate::from_pem("other-pem-data")));
367 assert_ne!(with_ca.connection_key(), with_other_ca.connection_key());
368 }
369}