pub mod jitter; use std::time::{Duration, SystemTime, UNIX_EPOCH}; pub use jitter::jitter; use crate::{Result, err}; #[inline] #[must_use] #[allow(clippy::as_conversions, clippy::cast_possible_truncation)] pub fn now_millis() -> u64 { UNIX_EPOCH .elapsed() .expect("positive duration after epoch") .as_millis() as u64 } /// Returns a capped linear retry delay for a failed attempt. #[must_use] pub fn linear_backoff(base: Duration, limit: Duration, attempt: u32) -> Duration { base.saturating_mul(attempt).min(limit) } /// Returns a saturated Unix-millisecond deadline after `duration`. #[must_use] pub fn millis_from_now(duration: Duration) -> u64 { add_millis(now_millis(), duration) } fn add_millis(now: u64, duration: Duration) -> u64 { now.saturating_add(u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)) } #[inline] pub fn parse_timepoint_ago(ago: &str) -> Result { timepoint_ago(parse_duration(ago)?) } #[inline] pub fn timepoint_ago(duration: Duration) -> Result { SystemTime::now() .checked_sub(duration) .ok_or_else(|| err!(Arithmetic("Duration {duration:?} is too large"))) } #[inline] pub fn timepoint_from_now(duration: Duration) -> Result { SystemTime::now() .checked_add(duration) .ok_or_else(|| err!(Arithmetic("Duration {duration:?} is too large"))) } #[inline] pub fn parse_duration(duration: &str) -> Result { cyborgtime::parse_duration(duration) .map_err(|error| err!("'{duration:?}' is not a valid duration string: {error:?}")) } #[must_use] pub fn rfc2822_from_seconds(epoch: i64) -> String { use chrono::{DateTime, Utc}; DateTime::::from_timestamp(epoch, 0) .unwrap_or_default() .to_rfc2822() } #[must_use] pub fn format(ts: SystemTime, str: &str) -> String { use chrono::{DateTime, Utc}; let dt: DateTime = ts.into(); dt.format(str).to_string() } #[must_use] #[allow(clippy::as_conversions, clippy::cast_possible_truncation, clippy::cast_sign_loss)] pub fn pretty(d: Duration) -> String { use Unit::*; let fmt = |w, u| { if w == 1 { format!("{w} {u}") } else { format!("{w} {u}s") } }; let gen64 = |w, u| fmt(w, u); let gen128 = |w, u| gen64(u64::try_from(w).expect("u128 to u64"), u); match whole_and_frac(d) { | (Days(whole), _) => gen64(whole, "day"), | (Hours(whole), _) => gen64(whole, "hour"), | (Mins(whole), _) => gen64(whole, "minute"), | (Secs(whole), _) => gen64(whole, "second"), | (Millis(whole), _) => gen128(whole, "millisecond"), | (Micros(whole), _) => gen128(whole, "microsecond"), | (Nanos(whole), _) => gen128(whole, "nanosecond"), } } /// Return a pair of (whole part, frac part) from a duration where. The whole /// part is the largest Unit containing a non-zero value, the frac part is a /// rational remainder left over. #[must_use] #[allow(clippy::as_conversions, clippy::cast_precision_loss)] pub fn whole_and_frac(d: Duration) -> (Unit, f64) { use Unit::*; let whole = whole_unit(d); (whole, match whole { | Days(_) => (d.as_secs() % 86_400) as f64 / 86_400.0, | Hours(_) => (d.as_secs() % 3_600) as f64 / 3_600.0, | Mins(_) => (d.as_secs() % 60) as f64 / 60.0, | Secs(_) => f64::from(d.subsec_millis()) / 1000.0, | Millis(_) => f64::from(d.subsec_micros()) / 1000.0, | Micros(_) => f64::from(d.subsec_nanos()) / 1000.0, | Nanos(_) => 0.0, }) } /// Return the largest Unit which represents the duration. The value is /// rounded-down, but never zero. #[must_use] pub fn whole_unit(d: Duration) -> Unit { use Unit::*; match d.as_secs() { | 86_400.. => Days(d.as_secs() / 86_400), | 3_600..=86_399 => Hours(d.as_secs() / 3_600), | 60..=3_599 => Mins(d.as_secs() / 60), | _ => match d.as_micros() { | 1_000_000.. => Secs(d.as_secs()), | 1_000..=999_999 => Millis(d.subsec_millis().into()), | _ => match d.as_nanos() { | 1_000.. => Micros(d.subsec_micros().into()), | _ => Nanos(d.subsec_nanos().into()), }, }, } } #[derive(Eq, PartialEq, Clone, Copy, Debug)] pub enum Unit { Days(u64), Hours(u64), Mins(u64), Secs(u64), Millis(u128), Micros(u128), Nanos(u128), } #[derive(Eq, PartialEq, Clone, Copy, Debug)] pub enum TimeDirection { Before, After, } /// Checks if `item_time` is before or after `time_boundary`. /// If both times are the same, it will return true for both directions, as the /// matching is inclusive. #[must_use] pub fn is_within_bounds( item_time: SystemTime, time_boundary: SystemTime, direction: TimeDirection, ) -> bool { match direction { | TimeDirection::Before => item_time <= time_boundary, | TimeDirection::After => item_time >= time_boundary, } } #[cfg(test)] mod tests { use std::time::Duration; use super::{add_millis, linear_backoff}; #[test] fn linear_backoff_scales_and_caps() { let base = Duration::from_secs(10); let limit = Duration::from_secs(25); assert_eq!(linear_backoff(base, limit, 0), Duration::ZERO); assert_eq!(linear_backoff(base, limit, 1), Duration::from_secs(10)); assert_eq!(linear_backoff(base, limit, 2), Duration::from_secs(20)); assert_eq!(linear_backoff(base, limit, 3), limit); assert_eq!(linear_backoff(base, limit, u32::MAX), limit); } #[test] fn millis_deadline_saturates() { assert_eq!(add_millis(10, Duration::from_millis(20)), 30); assert_eq!(add_millis(u64::MAX, Duration::from_millis(1)), u64::MAX); assert_eq!(add_millis(0, Duration::from_secs(u64::MAX)), u64::MAX); } }