//! Backoff arithmetic and `Retry-After` parsing. use std::collections::hash_map::RandomState; use std::hash::{BuildHasher, Hasher}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; /// A random `Duration` in `[0, ceiling]` — *full jitter*. /// /// The randomness is not decoration: without it, N clients that fail together /// come back together and reproduce the spike that took the service down. /// /// The source is the standard library's hash seed mixed with the current time, /// which is plenty for spreading retries and is not suitable for anything /// cryptographic. pub(crate) fn jitter(ceiling: Duration) -> Duration { let mut hasher = RandomState::new().build_hasher(); hasher.write_u128( SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_nanos(), ); let fraction = (hasher.finish() >> 11) as f64 / (1u64 << 53) as f64; ceiling.mul_f64(fraction) } /// Parse the `Retry-After` header in both RFC 9110 forms. /// /// It may arrive as a number of seconds (`"120"`) or as an HTTP date /// (`"Wed, 21 Oct 2026 07:28:00 GMT"`). Handling only the first is the common /// bug, and it makes a client ignore exactly the wait request that matters /// most. A date already in the past yields `Duration::ZERO`; anything /// unparseable yields `None`. pub(crate) fn parse_retry_after(value: &str) -> Option { let value = value.trim(); if value.is_empty() { return None; } if let Ok(seconds) = value.parse::() { if seconds.is_finite() { return Some(Duration::try_from_secs_f64(seconds.max(0.0)).unwrap_or(Duration::ZERO)); } } let target = http_date_to_unix(value)?; let now = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs() as i64; Some(Duration::from_secs((target - now).max(0) as u64)) } /// Seconds since the Unix epoch for an IMF-fixdate, the form HTTP servers must /// send: `Wed, 21 Oct 2026 07:28:00 GMT`. The two obsolete formats RFC 9110 /// still mentions are not accepted. fn http_date_to_unix(value: &str) -> Option { let rest = value.split_once(", ").map(|(_, r)| r).unwrap_or(value); let mut parts = rest.split_whitespace(); let day: i64 = parts.next()?.parse().ok()?; let month = month_number(parts.next()?)?; let year: i64 = parts.next()?.parse().ok()?; let mut clock = parts.next()?.split(':'); let hour: i64 = clock.next()?.parse().ok()?; let minute: i64 = clock.next()?.parse().ok()?; let second: i64 = clock.next()?.parse().ok()?; if !(1..=31).contains(&day) || hour > 23 || minute > 59 || second > 60 { return None; } Some(days_from_civil(year, month, day) * 86_400 + hour * 3_600 + minute * 60 + second) } fn month_number(name: &str) -> Option { const MONTHS: [&str; 12] = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ]; MONTHS.iter().position(|m| *m == name).map(|i| i as i64 + 1) } /// Days between 1970-01-01 and the given proleptic Gregorian date. fn days_from_civil(year: i64, month: i64, day: i64) -> i64 { let year = if month <= 2 { year - 1 } else { year }; let era = if year >= 0 { year } else { year - 399 } / 400; let year_of_era = year - era * 400; let month_shifted = (month + 9) % 12; let day_of_year = (153 * month_shifted + 2) / 5 + day - 1; let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year; era * 146_097 + day_of_era - 719_468 } #[cfg(test)] mod tests { use super::{days_from_civil, http_date_to_unix, jitter, parse_retry_after}; use std::time::Duration; #[test] fn reads_plain_seconds() { assert_eq!(parse_retry_after(" 7 "), Some(Duration::from_secs(7))); } #[test] fn negative_seconds_become_zero() { assert_eq!(parse_retry_after("-5"), Some(Duration::ZERO)); } #[test] fn reads_an_http_date() { let epoch = http_date_to_unix("Wed, 21 Oct 2026 07:28:00 GMT").unwrap(); assert_eq!(epoch, 1_792_567_680); } #[test] fn a_date_in_the_past_is_zero() { assert_eq!( parse_retry_after("Thu, 01 Jan 1970 00:00:00 GMT"), Some(Duration::ZERO) ); } #[test] fn rejects_nonsense() { assert_eq!(parse_retry_after("soon"), None); assert_eq!(parse_retry_after(""), None); } #[test] fn civil_days_match_known_dates() { assert_eq!(days_from_civil(1970, 1, 1), 0); assert_eq!(days_from_civil(2000, 3, 1), 11_017); } #[test] fn jitter_stays_within_the_ceiling() { let ceiling = Duration::from_millis(50); for _ in 0..100 { assert!(jitter(ceiling) <= ceiling); } } }