Skip to content
Open
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
11 changes: 11 additions & 0 deletions doc/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,17 @@ increasing jitter helps minimise collisions.
raise Exception


For a gentler backoff curve than pure exponential, Fibonacci backoff increases
wait times following the Fibonacci sequence (1, 2, 3, 5, 8, 13, …).

.. testcode::

@retry(wait=wait_fibonacci(max=60))
def wait_fibonacci_up_to_60s():
print("Wait 1s, 2s, 3s, 5s, 8s, 13s, ... up to 60s between retries")
raise Exception


Sometimes it's necessary to build a chain of backoffs.

.. testcode::
Expand Down
6 changes: 6 additions & 0 deletions releasenotes/notes/wait-fibonacci-55c6d584f5a34432.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
features:
- |
Add ``wait_fibonacci`` strategy that applies Fibonacci backoff.
This is a stateful wait strategy where wait times follow the Fibonacci
sequence (1, 2, 3, 5, 8, 13, ...) up to an optional maximum.
2 changes: 2 additions & 0 deletions tenacity/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
wait_exception,
wait_exponential,
wait_exponential_jitter,
wait_fibonacci,
wait_fixed,
wait_incrementing,
wait_none,
Expand Down Expand Up @@ -797,6 +798,7 @@ def wrap(f: t.Callable[P, R]) -> _RetryDecorated[P, R]:
"wait_exception",
"wait_exponential",
"wait_exponential_jitter",
"wait_fibonacci",
"wait_fixed",
"wait_full_jitter",
"wait_incrementing",
Expand Down
45 changes: 45 additions & 0 deletions tenacity/wait.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,51 @@ def __call__(self, retry_state: "RetryCallState") -> float:
return max(max(0, self.min), min(result, self.max))


class wait_fibonacci(wait_base):
"""Wait strategy that applies a Fibonacci backoff.

Fibonacci backoff is a compromise between linear and exponential backoff.
It increases the wait time more slowly than a pure exponential strategy,
making it effective for scenarios where a resource might be unavailable
for a moderate duration and you want to minimize latency without
hammering the server.

.. note::
This strategy is stateful. The wait intervals increase with each
subsequent retry performed by this specific instance.

Example::

# Wait times: 1s, 2s, 3s, 5s, 8s, 13s... up to 60s
wait_fibonacci(max=60)
"""

def __init__(
self,
max: _utils.time_unit_type = _utils.MAX_WAIT,
) -> None:
"""Initialize the wait strategy.

:param max: The maximum number of seconds to wait.
"""
self.max = _utils.to_seconds(max)
self._serie = [0, 1]

def compute_next_value(self) -> int:
"""Compute the next value in the Fibonacci sequence."""
next_value = self._serie[-1] + self._serie[-2]
self._serie.append(next_value)
return next_value

def __call__(self, retry_state: "RetryCallState") -> float:
"""Return the next wait time from the Fibonacci sequence."""
try:
result = self.compute_next_value()
except OverflowError:
return self.max
return min(result, self.max)


class wait_random_exponential(wait_exponential):
"""Random wait with exponentially widening window.

Expand Down
49 changes: 49 additions & 0 deletions tests/test_tenacity.py
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,55 @@ def test_wait_exponential_jitter(self) -> None:
fn = tenacity.wait_exponential_jitter()
fn(make_retry_state(0, 0))

def test_wait_fibonacci(self) -> None:
# wait_fibonacci is purely stateful: it ignores retry_state and
# advances its internal Fibonacci sequence on each call.
fn = tenacity.wait_fibonacci()
expected = [1, 2, 3, 5, 8, 13, 21, 34]
for i, exp in enumerate(expected, 1):
self.assertEqual(fn(make_retry_state(i, 0)), exp)

def test_wait_fibonacci_with_max_wait(self) -> None:
for max_ in (10, datetime.timedelta(seconds=10)):
with self.subTest():
fn = tenacity.wait_fibonacci(max=max_)
# 1, 2, 3, 5, 8, 10, 10, 10
self.assertEqual(fn(make_retry_state(1, 0)), 1)
self.assertEqual(fn(make_retry_state(2, 0)), 2)
self.assertEqual(fn(make_retry_state(3, 0)), 3)
self.assertEqual(fn(make_retry_state(4, 0)), 5)
self.assertEqual(fn(make_retry_state(5, 0)), 8)
self.assertEqual(fn(make_retry_state(6, 0)), 10)
self.assertEqual(fn(make_retry_state(7, 0)), 10)
self.assertEqual(fn(make_retry_state(8, 0)), 10)

def test_wait_fibonacci_multiple_invocations(self) -> None:
sleep_intervals: list[float] = []
r = Retrying(
sleep=sleep_intervals.append,
wait=tenacity.wait_fibonacci(max=60),
stop=tenacity.stop_after_attempt(6),
retry=tenacity.retry_if_result(lambda x: x == 1),
)

@r.wraps
def always_return_1() -> int:
return 1

self.assertRaises(tenacity.RetryError, always_return_1)
self.assertEqual(sleep_intervals, [1, 2, 3, 5, 8])
sleep_intervals[:] = []

# Second invocation: wait_fibonacci is stateful, sequence continues.
# The first run consumed 6 Fibonacci values (5 sleeps + 1 extra wait
# call on the final attempt), so the second run picks up at position 7.
self.assertRaises(tenacity.RetryError, always_return_1)
self.assertEqual(sleep_intervals, [21, 34, 55, 60, 60])

def test_wait_fibonacci_default_args(self) -> None:
fn = tenacity.wait_fibonacci()
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):
Expand Down