qubit_retry/options/retry_jitter.rs
1/*******************************************************************************
2 *
3 * Copyright (c) 2025 - 2026.
4 * Haixing Hu, Qubit Co. Ltd.
5 *
6 * All rights reserved.
7 *
8 ******************************************************************************/
9//! Retry jitter applied on top of a base [`crate::RetryDelay`].
10//!
11//! After [`crate::RetryDelay`] yields a base sleep duration for the next attempt,
12//! [`RetryJitter`] optionally perturbs it so concurrent retries do not align on the
13//! same schedule.
14//!
15//! # Text interchange
16//!
17//! [`std::fmt::Display`] and [`std::str::FromStr`] use the same grammar:
18//!
19//! - `none` in any ASCII letter case (leading/trailing ASCII whitespace trimmed).
20//! - `factor:` followed by a floating-point literal in **`[0.0, 1.0]`**; optional
21//! ASCII whitespace is allowed after the colon.
22//!
23//! The `factor:` prefix itself is **case-sensitive**. See
24//! [`crate::constants::DEFAULT_RETRY_JITTER`] for the library default string.
25//!
26//! Author: Haixing Hu
27
28use std::str::FromStr;
29use std::time::Duration;
30
31use parse_display::{Display, DisplayFormat, FromStr as DeriveFromStr, FromStrFormat, ParseError};
32use rand::RngExt;
33use serde::{Deserialize, Serialize};
34
35use crate::RetryDelay;
36use crate::constants::DEFAULT_RETRY_JITTER;
37
38/// Jitter strategy applied after a base [`crate::RetryDelay`] has been calculated.
39///
40/// Supports [`RetryJitter::None`] and symmetric [`RetryJitter::Factor`] jitter.
41/// After randomization, delays are clamped to **non-negative** values.
42#[derive(Debug, Clone, Copy, PartialEq, Display, DeriveFromStr, Serialize, Deserialize)]
43pub enum RetryJitter {
44 /// No jitter: [`RetryJitter::apply`] returns the base delay unchanged.
45 #[display("none")]
46 #[from_str(regex = r"(?i)\s*none\s*")]
47 None,
48
49 /// Symmetric relative jitter around the base delay.
50 ///
51 /// The inner `f64` is the relative half-span: jitter is drawn uniformly from
52 /// `[-base * factor, base * factor]` nanoseconds (see [`RetryJitter::apply`]).
53 /// It must be finite and lie in **`[0.0, 1.0]`** for validated configurations.
54 #[display("factor:{0}")]
55 #[from_str(regex = r"\s*factor:\s*(?<0>\S(?:.*\S)?)\s*")]
56 Factor(#[display(with = RetryJitterFactorFormat)] f64),
57}
58
59/// Formats jitter factors as `f64` text and parses with range validation.
60struct RetryJitterFactorFormat;
61
62impl DisplayFormat<f64> for RetryJitterFactorFormat {
63 /// Writes the factor using the default `f64` formatter.
64 ///
65 /// # Parameters
66 /// - `f`: Output formatter.
67 /// - `value`: Factor value.
68 ///
69 /// # Returns
70 /// `Ok(())` on success, or [`std::fmt::Error`] if formatting fails.
71 ///
72 /// # Errors
73 /// Returns [`std::fmt::Error`] only if the formatter rejects output.
74 fn write(&self, f: &mut std::fmt::Formatter<'_>, value: &f64) -> std::fmt::Result {
75 write!(f, "{value}")
76 }
77}
78
79impl FromStrFormat<f64> for RetryJitterFactorFormat {
80 /// Error returned by factor parsing.
81 type Err = ParseError;
82
83 /// Parses and validates a factor in range `[0.0, 1.0]`.
84 ///
85 /// # Parameters
86 /// - `s`: Raw factor text captured by `parse-display`.
87 ///
88 /// # Returns
89 /// The parsed factor.
90 ///
91 /// # Errors
92 /// Returns [`ParseError`] when the input is not a valid `f64` or lies outside
93 /// `[0.0, 1.0]`, including non-finite values.
94 fn parse(&self, s: &str) -> Result<f64, Self::Err> {
95 let value = s
96 .parse::<f64>()
97 .map_err(|_| ParseError::with_message("invalid retry jitter factor"))?;
98 if !(0.0..=1.0).contains(&value) {
99 return Err(ParseError::with_message(
100 "retry jitter factor must be in range [0.0, 1.0]",
101 ));
102 }
103 Ok(value)
104 }
105}
106
107impl RetryJitter {
108 /// Creates a no-jitter strategy.
109 ///
110 /// # Parameters
111 /// This function has no parameters.
112 ///
113 /// # Returns
114 /// A [`RetryJitter::None`] strategy.
115 ///
116 /// # Errors
117 /// This function does not return errors.
118 #[inline]
119 pub fn none() -> Self {
120 Self::None
121 }
122
123 /// Creates a symmetric relative jitter strategy.
124 ///
125 /// Validation requires `factor` to be finite and within `[0.0, 1.0]`.
126 ///
127 /// # Parameters
128 /// - `factor`: Relative jitter range. For example, `0.2` samples from
129 /// `base +/- 20%`.
130 ///
131 /// # Returns
132 /// A [`RetryJitter::Factor`] strategy.
133 ///
134 /// # Errors
135 /// This constructor does not validate `factor`; use [`RetryJitter::validate`]
136 /// before applying values that come from configuration or user input.
137 #[inline]
138 pub fn factor(factor: f64) -> Self {
139 Self::Factor(factor)
140 }
141
142 /// Applies jitter to a base delay.
143 ///
144 /// For [`RetryJitter::None`], returns `base` unchanged.
145 ///
146 /// For [`RetryJitter::Factor`], if `factor <= 0.0` or `base` is zero, returns
147 /// `base` unchanged. Otherwise draws a uniform sample from the inclusive range
148 /// `[-base * factor, base * factor]` in nanosecond space, adds it to the base,
149 /// then clamps the result to **at least zero** (truncating the sum to `u64`
150 /// nanoseconds). When `base` exceeds `u64::MAX` nanoseconds, this function
151 /// returns `base` unchanged to avoid lossy downcasts.
152 ///
153 /// # Parameters
154 /// - `base`: Base delay calculated by [`crate::RetryDelay`].
155 ///
156 /// # Returns
157 /// The jittered delay, never below zero.
158 ///
159 /// # Errors
160 /// This function does not return errors.
161 ///
162 /// # Panics
163 /// This function does not panic for non-finite factors. Non-finite values
164 /// gracefully fall back to returning `base`.
165 pub fn apply(&self, base: Duration) -> Duration {
166 match self {
167 Self::None => base,
168 Self::Factor(factor) if !factor.is_finite() || *factor <= 0.0 || base.is_zero() => base,
169 Self::Factor(factor) => {
170 let base_nanos_u128 = base.as_nanos();
171 if base_nanos_u128 > u64::MAX as u128 {
172 return base;
173 }
174 let base_nanos = base_nanos_u128 as f64;
175 let span = base_nanos * factor;
176 let mut rng = rand::rng();
177 let jitter = rng.random_range(-span..=span);
178 let nanos = (base_nanos + jitter).clamp(0.0, u64::MAX as f64) as u64;
179 Duration::from_nanos(nanos)
180 }
181 }
182 }
183
184 /// Calculates and jitters the delay for one retry attempt.
185 ///
186 /// This method combines base-delay strategy selection and jitter application
187 /// into one step.
188 ///
189 /// # Parameters
190 /// - `delay_strategy`: Base delay strategy used to calculate the attempt
191 /// delay.
192 /// - `attempt`: Failed-attempt index passed to
193 /// [`RetryDelay::base_delay`].
194 ///
195 /// # Returns
196 /// The delay for the attempt after jitter is applied.
197 ///
198 /// # Errors
199 /// This function does not return errors.
200 ///
201 /// # Panics
202 /// This function does not panic for non-finite factors. Non-finite values
203 /// gracefully fall back to returning the base delay.
204 pub fn delay_for_attempt(&self, delay_strategy: &RetryDelay, attempt: u32) -> Duration {
205 let base_delay = delay_strategy.base_delay(attempt);
206 self.apply(base_delay)
207 }
208
209 /// Validates jitter parameters for use with executors and options.
210 ///
211 /// [`RetryJitter::None`] is always valid. For [`RetryJitter::Factor`], the factor
212 /// must be finite and satisfy **`0.0 <= factor <= 1.0`** (endpoints included).
213 ///
214 /// # Returns
215 /// `Ok(())` when the jitter configuration is usable.
216 ///
217 /// # Parameters
218 /// This method has no parameters.
219 ///
220 /// # Errors
221 /// Returns an error when the factor is negative, greater than `1.0`, NaN,
222 /// or infinite.
223 pub fn validate(&self) -> Result<(), String> {
224 match self {
225 Self::None => Ok(()),
226 Self::Factor(factor) => {
227 if !factor.is_finite() || *factor < 0.0 || *factor > 1.0 {
228 Err("jitter factor must be finite and in range [0.0, 1.0]".to_string())
229 } else {
230 Ok(())
231 }
232 }
233 }
234 }
235}
236
237impl Default for RetryJitter {
238 /// Creates the default jitter strategy.
239 ///
240 /// # Returns
241 /// The value obtained by parsing [`crate::constants::DEFAULT_RETRY_JITTER`]
242 /// using [`RetryJitter::from_str`].
243 ///
244 /// # Parameters
245 /// This function has no parameters.
246 ///
247 /// # Errors
248 /// This function does not return errors.
249 ///
250 /// # Panics
251 /// Panics if [`crate::constants::DEFAULT_RETRY_JITTER`] is not a valid
252 /// [`RetryJitter`] string. That indicates a crate bug, not a caller mistake.
253 #[inline]
254 fn default() -> Self {
255 Self::from_str(DEFAULT_RETRY_JITTER)
256 .expect("DEFAULT_RETRY_JITTER must be a valid RetryJitter string")
257 }
258}