diff --git a/releasenotes/notes/wait-exponential-jitter-min-timedelta-a8e3c1f4b7d29e50.yaml b/releasenotes/notes/wait-exponential-jitter-min-timedelta-a8e3c1f4b7d29e50.yaml new file mode 100644 index 00000000..38adcbc6 --- /dev/null +++ b/releasenotes/notes/wait-exponential-jitter-min-timedelta-a8e3c1f4b7d29e50.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add ``min`` parameter to ``wait_exponential_jitter`` to set a minimum wait + time floor, consistent with ``wait_exponential``. Also accept ``timedelta`` + for ``max``, ``jitter``, and ``min`` parameters. diff --git a/tenacity/wait.py b/tenacity/wait.py index b28b7886..92fa1f4a 100644 --- a/tenacity/wait.py +++ b/tenacity/wait.py @@ -240,26 +240,28 @@ def __call__(self, retry_state: "RetryCallState") -> float: class wait_exponential_jitter(wait_base): """Wait strategy that applies exponential backoff and jitter. - It allows for a customized initial wait, maximum wait and jitter. + It allows for a customized initial wait, maximum wait, jitter and minimum. This implements the strategy described here: https://cloud.google.com/storage/docs/retry-strategy - The wait time is min(initial * 2**n + random.uniform(0, jitter), maximum) + The wait time is max(min, min(initial * 2**n + random.uniform(0, jitter), maximum)) where n is the retry count. """ def __init__( self, initial: float = 1, - max: float = _utils.MAX_WAIT, + max: _utils.time_unit_type = _utils.MAX_WAIT, exp_base: float = 2, - jitter: float = 1, + jitter: _utils.time_unit_type = 1, + min: _utils.time_unit_type = 0, ) -> None: self.initial = initial - self.max = max + self.max = _utils.to_seconds(max) self.exp_base = exp_base - self.jitter = jitter + self.jitter = _utils.to_seconds(jitter) + self.min = _utils.to_seconds(min) def __call__(self, retry_state: "RetryCallState") -> float: jitter = random.uniform(0, self.jitter) @@ -268,4 +270,4 @@ def __call__(self, retry_state: "RetryCallState") -> float: result = self.initial * exp + jitter except OverflowError: result = self.max - return max(0, min(result, self.max)) + return max(max(0, self.min), min(result, self.max)) diff --git a/tests/test_tenacity.py b/tests/test_tenacity.py index efefba8c..7061cd74 100644 --- a/tests/test_tenacity.py +++ b/tests/test_tenacity.py @@ -617,6 +617,29 @@ def test_wait_exponential_jitter(self) -> None: fn = tenacity.wait_exponential_jitter() fn(make_retry_state(0, 0)) + def test_wait_exponential_jitter_min(self) -> None: + fn = tenacity.wait_exponential_jitter(initial=1, max=60, jitter=1, min=5) + for _ in range(1000): + # Even for attempt 1 (base wait=1 + jitter 0..1 = 1..2), min=5 applies + self._assert_inclusive_range(fn(make_retry_state(1, 0)), 5, 5) + self._assert_inclusive_range(fn(make_retry_state(2, 0)), 5, 5) + self._assert_inclusive_range(fn(make_retry_state(3, 0)), 5, 5) + # For attempt 4, base wait=8 + jitter 0..1 = 8..9, above min + self._assert_inclusive_range(fn(make_retry_state(4, 0)), 8, 9) + + def test_wait_exponential_jitter_timedelta(self) -> None: + from datetime import timedelta + + fn = tenacity.wait_exponential_jitter( + max=timedelta(seconds=60), + jitter=timedelta(seconds=1), + min=timedelta(seconds=5), + ) + for _ in range(1000): + self._assert_inclusive_range(fn(make_retry_state(1, 0)), 5, 5) + self._assert_inclusive_range(fn(make_retry_state(5, 0)), 16, 17) + self.assertEqual(fn(make_retry_state(7, 0)), 60) + def test_wait_retry_state_attributes(self) -> None: class ExtractCallState(Exception): pass