From 5790144853a0172f75ca5f6b92a6c802026f9742 Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Wed, 11 Mar 2026 14:36:07 +0100 Subject: [PATCH] feat: add `min` param and `timedelta` support to `wait_exponential_jitter` `wait_exponential` already supports `min` and `timedelta` for min/max. `wait_exponential_jitter` lacked both, which was an inconsistency users asked for in #426. - Accept `timedelta` for `max`, `jitter`, and `min` parameters - Add `min` parameter to set a minimum wait time floor - Convert all time parameters via `_utils.to_seconds()` in `__init__` Co-Authored-By: Claude Opus 4.6 Change-Id: Ic02ae68b863cc0d1cd5a3b7874006c193c0c14a8 --- ...jitter-min-timedelta-a8e3c1f4b7d29e50.yaml | 6 +++++ tenacity/wait.py | 16 +++++++------ tests/test_tenacity.py | 23 +++++++++++++++++++ 3 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/wait-exponential-jitter-min-timedelta-a8e3c1f4b7d29e50.yaml 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