Skip to content

Commit 39ef35b

Browse files
authored
chore: consolidate duplicate backoff logic into Backoff class (#2660)
1 parent e6451ef commit 39ef35b

3 files changed

Lines changed: 16 additions & 24 deletions

File tree

cognite/client/_http_client.py

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import asyncio
44
import functools
55
import logging
6-
import random
76
from collections.abc import (
87
AsyncIterable,
98
AsyncIterator,
@@ -28,6 +27,7 @@
2827
CogniteRequestError,
2928
)
3029
from cognite.client.response import CogniteHTTPResponse
30+
from cognite.client.utils._retry import Backoff
3131

3232
logger = logging.getLogger(__name__)
3333

@@ -132,24 +132,18 @@ def __init__(self, url: str, config: AsyncHTTPClientWithRetryConfig) -> None:
132132
self.status = self.read = self.connect = 0
133133
self.last_failed_reason = ""
134134
self.total_time_in_backoff = 0.0
135+
self._backoff = Backoff(
136+
multiplier=config.backoff_factor,
137+
max_wait=float(config.max_backoff_seconds),
138+
min_wait=0.1,
139+
)
135140

136141
@property
137142
def total(self) -> int:
138143
return self.status + self.read + self.connect
139144

140-
def get_backoff_time(self) -> float:
141-
"""
142-
Computes the randomized delay (backoff time) before the next retry attempt.
143-
144-
This method implements the **Exponential Backoff with Full Jitter** strategy,
145-
with one addition, a small "initial floor" to avoid immediate retries.
146-
"""
147-
base = self.config.backoff_factor * 2**self.total
148-
cap = min(base, self.config.max_backoff_seconds)
149-
return random.uniform(0.1, cap)
150-
151145
async def back_off(self) -> None:
152-
backoff_time = self.get_backoff_time()
146+
backoff_time = next(self._backoff)
153147
self.total_time_in_backoff += backoff_time
154148
# We put logging here, since this is always called before retrying
155149
logger.debug(

cognite/client/utils/_retry.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,24 @@ class Backoff(Iterator[float]):
99
described in this post: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
1010
1111
Args:
12-
multiplier (float): No description.
13-
max_wait (float): No description.
14-
base (int): No description."""
12+
multiplier (float): Scales the exponential curve; the cap after n attempts is ``multiplier * base**n``.
13+
max_wait (float): Hard ceiling on the sampled wait in seconds.
14+
base (int): Exponential base; 2 gives classic doubling behaviour.
15+
min_wait (float): Lower bound of the uniform sample, useful when an immediate retry is undesirable."""
1516

16-
def __init__(self, multiplier: float = 0.5, max_wait: float = 60.0, base: int = 2) -> None:
17+
def __init__(self, multiplier: float = 0.5, max_wait: float = 60.0, base: int = 2, min_wait: float = 0.0) -> None:
1718
self._multiplier = multiplier
1819
self._max_wait = max_wait
1920
self._base = base
21+
self._min_wait = min_wait
2022
self._past_attempts = 0
2123

2224
def __next__(self) -> float:
2325
# 100 is an arbitrary limit at which point most sensible parameters are likely to
2426
# be capped by max anyway.
25-
wait = uniform(0, min(self._multiplier * (self._base ** min(100, self._past_attempts)), self._max_wait))
27+
base = self._multiplier * (self._base ** min(100, self._past_attempts))
28+
cap = max(self._min_wait, min(base, self._max_wait))
29+
wait = uniform(self._min_wait, cap)
2630
self._past_attempts += 1
2731
return wait
2832

tests/tests_unit/test_http_client.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,6 @@ def test_correct_retry_of_status(self, default_config: AsyncHTTPClientWithRetryC
105105
rt.config.status_codes_to_retry.add(500)
106106
assert rt.should_retry_status_code(make_http_status_error(500)) is True
107107

108-
def test_get_backoff_time(self, default_config: AsyncHTTPClientWithRetryConfig) -> None:
109-
rt = RetryTracker(URL, default_config)
110-
for i in range(10):
111-
rt.read += 1
112-
assert 0 <= rt.get_backoff_time() <= default_config.max_backoff_seconds
113-
114108
def test_is_auto_retryable(self, default_config: AsyncHTTPClientWithRetryConfig) -> None:
115109
default_config._max_retries_status = 1
116110
rt = RetryTracker(URL, default_config)

0 commit comments

Comments
 (0)