Skip to content

Commit

Permalink
Implement a wait.wait_exponential_jitter per Google's storage guide (#…
Browse files Browse the repository at this point in the history
…351)

* Implement a wait.wait_exponential_jitter per Google's storage retry guide

* Define a ClientError so Sphinx does not fail

* Fix spelling typos

* Simplify typing, replacing `int | float` with `float`

* Drop needless `#noqa`

Co-authored-by: Isaac Good <[email protected]>
  • Loading branch information
IsaacG and Isaac Good authored Apr 27, 2022
1 parent a858965 commit da1bfc9
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
features:
- |
Implement a wait.wait_exponential_jitter per Google's storage retry guide.
See https://cloud.google.com/storage/docs/retry-strategy
1 change: 1 addition & 0 deletions tenacity/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
from .wait import wait_random # noqa
from .wait import wait_random_exponential # noqa
from .wait import wait_random_exponential as wait_full_jitter # noqa
from .wait import wait_exponential_jitter # noqa

# Import all built-in before strategies for easier usage.
from .before import before_log # noqa
Expand Down
34 changes: 34 additions & 0 deletions tenacity/wait.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,37 @@ class wait_random_exponential(wait_exponential):
def __call__(self, retry_state: "RetryCallState") -> float:
high = super().__call__(retry_state=retry_state)
return random.uniform(0, high)


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.
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)
where n is the retry count.
"""

def __init__(
self,
initial: float = 1,
max: float = _utils.MAX_WAIT, # noqa
exp_base: float = 2,
jitter: float = 1,
) -> None:
self.initial = initial
self.max = max
self.exp_base = exp_base
self.jitter = jitter

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
except OverflowError:
result = self.max
return max(0, min(result, self.max))
22 changes: 22 additions & 0 deletions tests/test_tenacity.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,28 @@ def mean(lst):
self._assert_inclusive_epsilon(mean(attempt[8]), 30, 2.56)
self._assert_inclusive_epsilon(mean(attempt[9]), 30, 2.56)

def test_wait_exponential_jitter(self):
fn = tenacity.wait_exponential_jitter(max=60)

for _ in range(1000):
self._assert_inclusive_range(fn(make_retry_state(1, 0)), 1, 2)
self._assert_inclusive_range(fn(make_retry_state(2, 0)), 2, 3)
self._assert_inclusive_range(fn(make_retry_state(3, 0)), 4, 5)
self._assert_inclusive_range(fn(make_retry_state(4, 0)), 8, 9)
self._assert_inclusive_range(fn(make_retry_state(5, 0)), 16, 17)
self._assert_inclusive_range(fn(make_retry_state(6, 0)), 32, 33)
self.assertEqual(fn(make_retry_state(7, 0)), 60)
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)
for _ in range(1000):
self.assertEqual(fn(make_retry_state(1, 0)), 5)

# Default arguments exist
fn = tenacity.wait_exponential_jitter()
fn(make_retry_state(0, 0))

def test_wait_retry_state_attributes(self):
class ExtractCallState(Exception):
pass
Expand Down

0 comments on commit da1bfc9

Please sign in to comment.