Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
deprecations:
- |
The ``initial`` parameter of ``wait_exponential_jitter`` is deprecated in
favor of ``multiplier``, for consistency with ``wait_exponential``. Passing
``initial`` still works but emits a ``DeprecationWarning``.
features:
- |
Add ``multiplier`` parameter to ``wait_exponential_jitter``, consistent
with ``wait_exponential``.
23 changes: 19 additions & 4 deletions tenacity/wait.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import abc
import random
import typing
import warnings

from tenacity import _utils

Expand Down Expand Up @@ -240,12 +241,12 @@ 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, jitter and minimum.
It allows for a customized multiplier, maximum wait, jitter and minimum.

This implements the strategy described here:
https://cloud.google.com/storage/docs/retry-strategy

The wait time is max(min, min(initial * 2**n + random.uniform(0, jitter), maximum))
The wait time is max(min, min(multiplier * 2**n + random.uniform(0, jitter), maximum))
where n is the retry count.
"""

Expand All @@ -256,8 +257,22 @@ def __init__(
exp_base: float = 2,
jitter: _utils.time_unit_type = 1,
min: _utils.time_unit_type = 0,
multiplier: float = 1,
) -> None:
self.initial = initial
if initial != 1 and multiplier != 1:
raise ValueError(
"Cannot specify both 'initial' and 'multiplier' — use 'multiplier' only"
)

Comment on lines +262 to +266
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ValueError guard only triggers when both arguments are non-default (initial != 1 and multiplier != 1). This means callers can still pass both parameters without an error when initial is explicitly provided as the default (e.g. wait_exponential_jitter(initial=1, multiplier=10)), which contradicts the PR's stated behavior. Consider using a sentinel/None default for initial (or another way to detect whether it was provided) so you can reliably raise whenever initial is specified alongside multiplier.

Copilot uses AI. Check for mistakes.
if initial != 1:
warnings.warn(
"The 'initial' parameter is deprecated, use 'multiplier' instead",
DeprecationWarning,
stacklevel=2,
)
Comment on lines +267 to +272
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deprecation warnings are currently only emitted when initial != 1. If a caller explicitly uses the deprecated parameter with the default value (e.g. initial=1), no DeprecationWarning is raised even though the deprecated API is being used. If the intent is to warn whenever initial is supplied, switch initial to a sentinel/None default and check initial is not None (then map it to multiplier).

Copilot uses AI. Check for mistakes.
multiplier = initial

self.multiplier = multiplier
self.max = _utils.to_seconds(max)
self.exp_base = exp_base
self.jitter = _utils.to_seconds(jitter)
Expand All @@ -267,7 +282,7 @@ def __call__(self, retry_state: "RetryCallState") -> float:
jitter = random.uniform(0, self.jitter)
try:
exp = self.exp_base ** (retry_state.attempt_number - 1)
result = self.initial * exp + jitter
result = self.multiplier * exp + jitter
except OverflowError:
result = self.max
return max(max(0, self.min), min(result, self.max))
20 changes: 19 additions & 1 deletion tests/test_tenacity.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,8 @@ def test_wait_exponential_jitter(self) -> None:
self.assertEqual(fn(make_retry_state(8, 0)), 60)
self.assertEqual(fn(make_retry_state(9, 0)), 60)

fn = tenacity.wait_exponential_jitter(10, 5)
with self.assertWarns(DeprecationWarning):
fn = tenacity.wait_exponential_jitter(10, 5)
for _ in range(1000):
self.assertEqual(fn(make_retry_state(1, 0)), 5)

Expand Down Expand Up @@ -640,6 +641,23 @@ def test_wait_exponential_jitter_timedelta(self) -> None:
self._assert_inclusive_range(fn(make_retry_state(5, 0)), 16, 17)
self.assertEqual(fn(make_retry_state(7, 0)), 60)

def test_wait_exponential_jitter_multiplier(self) -> None:
fn = tenacity.wait_exponential_jitter(multiplier=10, max=60, jitter=0)
self.assertEqual(fn(make_retry_state(1, 0)), 10)
self.assertEqual(fn(make_retry_state(2, 0)), 20)
self.assertEqual(fn(make_retry_state(3, 0)), 40)
self.assertEqual(fn(make_retry_state(4, 0)), 60)

def test_wait_exponential_jitter_initial_deprecated(self) -> None:
with self.assertWarns(DeprecationWarning):
fn = tenacity.wait_exponential_jitter(initial=10, max=60, jitter=0)
self.assertEqual(fn(make_retry_state(1, 0)), 10)
self.assertEqual(fn(make_retry_state(2, 0)), 20)

def test_wait_exponential_jitter_initial_and_multiplier_raises(self) -> None:
with self.assertRaises(ValueError):
tenacity.wait_exponential_jitter(initial=5, multiplier=10)

def test_wait_retry_state_attributes(self) -> None:
class ExtractCallState(Exception):
pass
Expand Down
Loading