1use std::fmt;
28use std::str::FromStr;
29
30use crate::config::{MillisNonZero, TimeoutConfig};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct BackoffInterval {
64 pub min_ms: MillisNonZero,
66 pub max_ms: MillisNonZero,
68}
69
70impl Default for BackoffInterval {
71 fn default() -> Self {
72 Self {
73 min_ms: MillisNonZero::new(250).unwrap(),
74 max_ms: MillisNonZero::new(60_000).unwrap(),
75 }
76 }
77}
78
79impl fmt::Display for BackoffInterval {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 let min = format_ms(self.min_ms.get() as u64);
82 let max = format_ms(self.max_ms.get() as u64);
83 write!(f, "{min}..{max}")
84 }
85}
86
87fn format_ms(ms: u64) -> String {
92 if ms == 0 {
93 return "0ms".to_string();
94 }
95 if ms.is_multiple_of(86_400_000) {
96 format!("{}d", ms / 86_400_000)
97 } else if ms.is_multiple_of(3_600_000) {
98 format!("{}h", ms / 3_600_000)
99 } else if ms.is_multiple_of(60_000) {
100 format!("{}m", ms / 60_000)
101 } else if ms.is_multiple_of(1_000) {
102 format!("{}s", ms / 1_000)
103 } else {
104 format!("{ms}ms")
105 }
106}
107
108impl From<BackoffInterval> for TimeoutConfig {
112 fn from(backoff: BackoffInterval) -> Self {
113 TimeoutConfig {
114 backoff,
115 ..TimeoutConfig::default()
116 }
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
126pub enum ParseError {
127 Empty,
129 MissingRangeSeparator,
131 InvalidMin(String),
133 InvalidMax(String),
135 ZeroMin,
137 ZeroMax,
139 MinOverflow(u64),
141 MaxOverflow(u64),
143 MinExceedsMax { min_ms: u64, max_ms: u64 },
145}
146
147impl fmt::Display for ParseError {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 match self {
150 Self::Empty => write!(f, "empty input"),
151 Self::MissingRangeSeparator => write!(f, "missing '..' range separator"),
152 Self::InvalidMin(s) => write!(f, "invalid min timeout: {s}"),
153 Self::InvalidMax(s) => write!(f, "invalid max timeout: {s}"),
154 Self::ZeroMin => write!(f, "min timeout must be > 0"),
155 Self::ZeroMax => write!(f, "max timeout must be > 0"),
156 Self::MinOverflow(v) => write!(f, "min timeout ({v}ms) exceeds u32::MAX"),
157 Self::MaxOverflow(v) => write!(f, "max timeout ({v}ms) exceeds u32::MAX"),
158 Self::MinExceedsMax { min_ms, max_ms } => {
159 write!(
160 f,
161 "min timeout ({min_ms}ms) exceeds max timeout ({max_ms}ms)"
162 )
163 }
164 }
165 }
166}
167
168impl std::error::Error for ParseError {}
169
170impl FromStr for BackoffInterval {
171 type Err = ParseError;
172
173 fn from_str(s: &str) -> Result<Self, Self::Err> {
174 let s = s.trim();
175 if s.is_empty() {
176 return Err(ParseError::Empty);
177 }
178
179 let (min_str, max_str) = s
180 .split_once("..")
181 .ok_or(ParseError::MissingRangeSeparator)?;
182
183 let min_raw =
184 parse_duration_ms(min_str.trim()).map_err(|e| ParseError::InvalidMin(e.to_string()))?;
185 let max_raw =
186 parse_duration_ms(max_str.trim()).map_err(|e| ParseError::InvalidMax(e.to_string()))?;
187
188 if min_raw == 0 {
189 return Err(ParseError::ZeroMin);
190 }
191 if max_raw == 0 {
192 return Err(ParseError::ZeroMax);
193 }
194 if min_raw > max_raw {
195 return Err(ParseError::MinExceedsMax {
196 min_ms: min_raw,
197 max_ms: max_raw,
198 });
199 }
200
201 let min_u32 = u32::try_from(min_raw).map_err(|_| ParseError::MinOverflow(min_raw))?;
202 let max_u32 = u32::try_from(max_raw).map_err(|_| ParseError::MaxOverflow(max_raw))?;
203
204 let min_ms = MillisNonZero::new(min_u32).unwrap();
206 let max_ms = MillisNonZero::new(max_u32).unwrap();
207
208 Ok(BackoffInterval { min_ms, max_ms })
209 }
210}
211
212#[derive(Debug, Clone, PartialEq, Eq)]
218pub(crate) enum DurationParseError {
219 Empty,
220 InvalidNumber(String),
221 UnknownUnit(String),
222 TruncatesToZero(String),
224}
225
226impl fmt::Display for DurationParseError {
227 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228 match self {
229 Self::Empty => write!(f, "empty duration string"),
230 Self::InvalidNumber(s) => write!(f, "invalid number: {s}"),
231 Self::UnknownUnit(s) => write!(f, "unknown unit: {s}"),
232 Self::TruncatesToZero(s) => {
233 write!(f, "duration '{s}' truncates to 0ms")
234 }
235 }
236 }
237}
238
239fn parse_duration_ms(s: &str) -> Result<u64, DurationParseError> {
259 let s = s.trim();
260 if s.is_empty() {
261 return Err(DurationParseError::Empty);
262 }
263
264 if s == "0" {
266 return Ok(0);
267 }
268
269 let num_end = s
271 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-' && c != '+')
272 .unwrap_or(s.len());
273
274 let num_str = s[..num_end].trim();
275 let unit_str = s[num_end..].trim();
276
277 if num_str.is_empty() {
278 return Err(DurationParseError::InvalidNumber(s.to_string()));
279 }
280 if unit_str.is_empty() {
281 return Err(DurationParseError::UnknownUnit(
282 "missing unit suffix".to_string(),
283 ));
284 }
285
286 let value: f64 = num_str
287 .parse()
288 .map_err(|_| DurationParseError::InvalidNumber(num_str.to_string()))?;
289
290 let factor_ns: f64 = match unit_str {
293 "nanoseconds" | "nanosecond" | "nanos" | "nano" | "nsecs" | "nsec" | "ns" => 1.0,
294 "microseconds" | "microsecond" | "micros" | "micro" | "usecs" | "usec" | "us"
295 | "\u{00B5}s" | "\u{00B5}secs" | "\u{00B5}sec" | "\u{03BC}s" | "\u{03BC}secs"
296 | "\u{03BC}sec" => 1_000.0,
297 "milliseconds" | "millisecond" | "millis" | "milli" | "msecs" | "msec" | "ms" => {
298 1_000_000.0
299 }
300 "seconds" | "second" | "secs" | "sec" | "s" => 1_000_000_000.0,
301 "minutes" | "minute" | "mins" | "min" | "m" => 60.0 * 1_000_000_000.0,
302 "hours" | "hour" | "hrs" | "hr" | "h" => 3_600.0 * 1_000_000_000.0,
303 "days" | "day" | "d" => 86_400.0 * 1_000_000_000.0,
304 _ => return Err(DurationParseError::UnknownUnit(unit_str.to_string())),
305 };
306
307 let total_ns = value * factor_ns;
308 let ms = (total_ns / 1_000_000.0).round() as u64;
309
310 if ms == 0 && value != 0.0 {
312 return Err(DurationParseError::TruncatesToZero(s.to_string()));
313 }
314
315 Ok(ms)
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
327 fn parse_milliseconds() {
328 assert_eq!(parse_duration_ms("10ms").unwrap(), 10);
329 assert_eq!(parse_duration_ms("100ms").unwrap(), 100);
330 assert_eq!(parse_duration_ms("1ms").unwrap(), 1);
331 assert_eq!(parse_duration_ms("0ms").unwrap(), 0);
332 }
333
334 #[test]
335 fn parse_seconds() {
336 assert_eq!(parse_duration_ms("1s").unwrap(), 1_000);
337 assert_eq!(parse_duration_ms("10s").unwrap(), 10_000);
338 assert_eq!(parse_duration_ms("1sec").unwrap(), 1_000);
339 assert_eq!(parse_duration_ms("2secs").unwrap(), 2_000);
340 assert_eq!(parse_duration_ms("1second").unwrap(), 1_000);
341 assert_eq!(parse_duration_ms("3seconds").unwrap(), 3_000);
342 }
343
344 #[test]
345 fn parse_fractional() {
346 assert_eq!(parse_duration_ms("0.5s").unwrap(), 500);
347 assert_eq!(parse_duration_ms("1.5s").unwrap(), 1_500);
348 assert_eq!(parse_duration_ms("0.1s").unwrap(), 100);
349 assert_eq!(parse_duration_ms("2.5ms").unwrap(), 3); }
351
352 #[test]
353 fn parse_minutes() {
354 assert_eq!(parse_duration_ms("1min").unwrap(), 60_000);
355 assert_eq!(parse_duration_ms("2mins").unwrap(), 120_000);
356 assert_eq!(parse_duration_ms("1m").unwrap(), 60_000);
357 assert_eq!(parse_duration_ms("1minute").unwrap(), 60_000);
358 assert_eq!(parse_duration_ms("3minutes").unwrap(), 180_000);
359 }
360
361 #[test]
362 fn parse_hours() {
363 assert_eq!(parse_duration_ms("1h").unwrap(), 3_600_000);
364 assert_eq!(parse_duration_ms("1hr").unwrap(), 3_600_000);
365 assert_eq!(parse_duration_ms("2hrs").unwrap(), 7_200_000);
366 assert_eq!(parse_duration_ms("1hour").unwrap(), 3_600_000);
367 assert_eq!(parse_duration_ms("3hours").unwrap(), 10_800_000);
368 }
369
370 #[test]
371 fn parse_days() {
372 assert_eq!(parse_duration_ms("1d").unwrap(), 86_400_000);
373 assert_eq!(parse_duration_ms("1day").unwrap(), 86_400_000);
374 assert_eq!(parse_duration_ms("2days").unwrap(), 172_800_000);
375 }
376
377 #[test]
378 fn parse_microseconds() {
379 assert_eq!(parse_duration_ms("1000us").unwrap(), 1);
380 assert_eq!(parse_duration_ms("500us").unwrap(), 1); assert_eq!(parse_duration_ms("1000\u{03BC}s").unwrap(), 1); assert_eq!(parse_duration_ms("1000\u{00B5}s").unwrap(), 1); assert_eq!(parse_duration_ms("1000usec").unwrap(), 1);
384 assert_eq!(parse_duration_ms("1000usecs").unwrap(), 1);
385 assert_eq!(parse_duration_ms("1000micro").unwrap(), 1);
386 assert_eq!(parse_duration_ms("1000micros").unwrap(), 1);
387 assert_eq!(parse_duration_ms("1000microsecond").unwrap(), 1);
388 assert_eq!(parse_duration_ms("1000microseconds").unwrap(), 1);
389 }
390
391 #[test]
392 fn parse_nanoseconds() {
393 assert_eq!(parse_duration_ms("1000000ns").unwrap(), 1);
394 assert_eq!(parse_duration_ms("1000000nsec").unwrap(), 1);
395 assert_eq!(parse_duration_ms("1000000nsecs").unwrap(), 1);
396 assert_eq!(parse_duration_ms("1000000nano").unwrap(), 1);
397 assert_eq!(parse_duration_ms("1000000nanos").unwrap(), 1);
398 assert_eq!(parse_duration_ms("1000000nanosecond").unwrap(), 1);
399 assert_eq!(parse_duration_ms("1000000nanoseconds").unwrap(), 1);
400 }
401
402 #[test]
403 fn parse_millisecond_aliases() {
404 assert_eq!(parse_duration_ms("10msec").unwrap(), 10);
405 assert_eq!(parse_duration_ms("10msecs").unwrap(), 10);
406 assert_eq!(parse_duration_ms("10milli").unwrap(), 10);
407 assert_eq!(parse_duration_ms("10millis").unwrap(), 10);
408 assert_eq!(parse_duration_ms("10millisecond").unwrap(), 10);
409 assert_eq!(parse_duration_ms("10milliseconds").unwrap(), 10);
410 }
411
412 #[test]
413 fn parse_with_spaces() {
414 assert_eq!(parse_duration_ms("10 ms").unwrap(), 10);
415 assert_eq!(parse_duration_ms(" 1 s ").unwrap(), 1_000);
416 }
417
418 #[test]
419 fn parse_bare_zero() {
420 assert_eq!(parse_duration_ms("0").unwrap(), 0);
421 }
422
423 #[test]
424 fn parse_truncates_to_zero() {
425 assert!(matches!(
426 parse_duration_ms("1ns"),
427 Err(DurationParseError::TruncatesToZero(_))
428 ));
429 }
430
431 #[test]
432 fn parse_unknown_unit() {
433 assert!(matches!(
434 parse_duration_ms("10xyz"),
435 Err(DurationParseError::UnknownUnit(_))
436 ));
437 }
438
439 #[test]
440 fn parse_missing_unit() {
441 assert!(matches!(
442 parse_duration_ms("42"),
443 Err(DurationParseError::UnknownUnit(_))
444 ));
445 }
446
447 #[test]
448 fn parse_empty() {
449 assert!(matches!(
450 parse_duration_ms(""),
451 Err(DurationParseError::Empty)
452 ));
453 }
454
455 #[test]
460 fn parse_basic_range() {
461 let b: BackoffInterval = "10ms..1s".parse().unwrap();
462 assert_eq!(b.min_ms.get(), 10);
463 assert_eq!(b.max_ms.get(), 1_000);
464 }
465
466 #[test]
467 fn parse_fractional_range() {
468 let b: BackoffInterval = "0.5s..1.5s".parse().unwrap();
469 assert_eq!(b.min_ms.get(), 500);
470 assert_eq!(b.max_ms.get(), 1_500);
471 }
472
473 #[test]
474 fn parse_with_spaces_around() {
475 let b: BackoffInterval = " 10ms .. 1s ".parse().unwrap();
476 assert_eq!(b.min_ms.get(), 10);
477 assert_eq!(b.max_ms.get(), 1_000);
478 }
479
480 #[test]
481 fn parse_same_min_max() {
482 let b: BackoffInterval = "100ms..100ms".parse().unwrap();
483 assert_eq!(b.min_ms.get(), 100);
484 assert_eq!(b.max_ms.get(), 100);
485 }
486
487 #[test]
488 fn parse_large_values() {
489 let b: BackoffInterval = "1h..3d".parse().unwrap();
490 assert_eq!(b.min_ms.get(), 3_600_000);
491 assert_eq!(b.max_ms.get(), 259_200_000);
492 }
493
494 #[test]
495 fn parse_mixed_units() {
496 let b: BackoffInterval = "100ms..1min".parse().unwrap();
497 assert_eq!(b.min_ms.get(), 100);
498 assert_eq!(b.max_ms.get(), 60_000);
499 }
500
501 #[test]
502 fn err_empty() {
503 assert_eq!(
504 "".parse::<BackoffInterval>().unwrap_err(),
505 ParseError::Empty
506 );
507 }
508
509 #[test]
510 fn err_missing_separator() {
511 assert_eq!(
512 "10ms".parse::<BackoffInterval>().unwrap_err(),
513 ParseError::MissingRangeSeparator
514 );
515 }
516
517 #[test]
518 fn err_zero_min() {
519 assert_eq!(
520 "0ms..1s".parse::<BackoffInterval>().unwrap_err(),
521 ParseError::ZeroMin
522 );
523 }
524
525 #[test]
526 fn err_min_exceeds_max() {
527 assert!(matches!(
528 "10s..1s".parse::<BackoffInterval>(),
529 Err(ParseError::MinExceedsMax { .. })
530 ));
531 }
532
533 #[test]
534 fn err_invalid_min() {
535 assert!(matches!(
536 "abc..1s".parse::<BackoffInterval>(),
537 Err(ParseError::InvalidMin(_))
538 ));
539 }
540
541 #[test]
542 fn err_invalid_max() {
543 assert!(matches!(
544 "10ms..abc".parse::<BackoffInterval>(),
545 Err(ParseError::InvalidMax(_))
546 ));
547 }
548
549 #[test]
554 fn display_basic() {
555 let b: BackoffInterval = "10ms..1min".parse().unwrap();
556 assert_eq!(b.to_string(), "10ms..1m");
557 }
558
559 #[test]
560 fn display_sub_second() {
561 let b: BackoffInterval = "500ms..1500ms".parse().unwrap();
562 assert_eq!(b.to_string(), "500ms..1500ms");
563 }
564
565 #[test]
566 fn display_round_trip() {
567 for original in &["10ms..60s", "250ms..1m", "1s..1h", "100ms..1d"] {
568 let b: BackoffInterval = original.parse().unwrap();
569 let displayed = b.to_string();
570 let reparsed: BackoffInterval = displayed.parse().unwrap();
571 assert_eq!(b, reparsed, "round-trip failed for {original}");
572 }
573 }
574
575 #[test]
580 fn into_timeout_config() {
581 let b: BackoffInterval = "50ms..30s".parse().unwrap();
582 let cfg: TimeoutConfig = b.into();
583 assert_eq!(cfg.backoff.min_ms.get(), 50);
584 assert_eq!(cfg.backoff.max_ms.get(), 30_000);
585 assert_eq!(cfg.quantile, 0.9999);
587 assert_eq!(cfg.safety_factor, 2.0);
588 }
589}