Skip to content
This repository was archived by the owner on Apr 1, 2026. It is now read-only.

Commit 9fece96

Browse files
committed
added TrackedBackoffGenerator
1 parent d155f8a commit 9fece96

3 files changed

Lines changed: 82 additions & 2 deletions

File tree

google/cloud/bigtable/data/_helpers.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from google.cloud.bigtable.data.read_rows_query import ReadRowsQuery
2424

2525
from google.api_core import exceptions as core_exceptions
26+
from google.api_core.retry import exponential_sleep_generator
2627
from google.api_core.retry import RetryFailureReason
2728
from google.cloud.bigtable.data.exceptions import RetryExceptionGroup
2829

@@ -248,3 +249,32 @@ def _get_retryable_errors(
248249
call_codes = table.default_mutate_rows_retryable_errors
249250

250251
return [_get_error_type(e) for e in call_codes]
252+
253+
254+
class TrackedBackoffGenerator:
255+
"""
256+
Generator class for exponential backoff sleep times.
257+
This implementation builds on top of api_core.retries.exponential_sleep_generator,
258+
adding the ability to retrieve previous values using get_attempt_backoff(idx).
259+
This is used by the Metrics class to track the sleep times used for each attempt.
260+
"""
261+
262+
def __init__(self, initial=0.01, maximum=60, multiplier=2):
263+
self.history = []
264+
self.subgenerator = exponential_sleep_generator(
265+
initial=initial, maximum=maximum, multiplier=multiplier
266+
)
267+
268+
def __iter__(self):
269+
return self
270+
271+
def __next__(self) -> float:
272+
next_backoff = next(self.subgenerator)
273+
self.history.append(next_backoff)
274+
return next_backoff
275+
276+
def get_attempt_backoff(self, attempt_idx) -> float:
277+
"""
278+
returns the backoff time for a specific attempt index, starting at 0.
279+
"""
280+
return self.history[attempt_idx]

google/cloud/bigtable/data/_metrics/data_model.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333
if TYPE_CHECKING:
3434
from google.cloud.bigtable.data._metrics.handlers._base import MetricsHandler
35-
from google.cloud.bigtable.data._helpers import BackoffGenerator
35+
from google.cloud.bigtable.data._helpers import TrackedBackoffGenerator
3636

3737

3838
LOGGER = logging.getLogger(__name__)
@@ -144,7 +144,7 @@ class ActiveOperationMetric:
144144

145145
op_type: OperationType
146146
uuid: str = str(uuid.uuid4())
147-
backoff_generator: BackoffGenerator | None = None
147+
backoff_generator: TrackedBackoffGenerator | None = None
148148
# keep monotonic timestamps for active operations
149149
start_time_ns: int = field(default_factory=time.monotonic_ns)
150150
active_attempt: ActiveAttemptMetric | None = None

tests/unit/data/test__helpers.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,3 +266,53 @@ def test_get_retryable_errors(self, input_codes, input_table, expected):
266266
setattr(fake_table, f"{key}_retryable_errors", input_table[key])
267267
result = _helpers._get_retryable_errors(input_codes, fake_table)
268268
assert result == expected
269+
270+
271+
class TestTrackedBackoffGenerator:
272+
273+
def test_tracked_backoff_generator_history(self):
274+
"""
275+
Should be able to retrieve historical results from backoff generator
276+
"""
277+
generator = _helpers.TrackedBackoffGenerator(initial=0, multiplier=2, maximum=10)
278+
got_list = [next(generator) for _ in range(20)]
279+
280+
# check all values are correct
281+
for i in range(19, 0, -1):
282+
assert generator.get_attempt_backoff(i) == got_list[i]
283+
# check a random value out of order
284+
assert generator.get_attempt_backoff(5) == got_list[5]
285+
286+
@mock.patch("random.uniform", side_effect=lambda a, b: b)
287+
def test_tracked_backoff_generator_defaults(self, mock_uniform):
288+
"""
289+
Should generate values with default parameters
290+
291+
initial=0.01, multiplier=2, maximum=60
292+
"""
293+
generator = _helpers.TrackedBackoffGenerator()
294+
expected_values = [0.01, 0.02, 0.04, 0.08, 0.16]
295+
for expected in expected_values:
296+
assert next(generator) == pytest.approx(expected)
297+
298+
@mock.patch("random.uniform", side_effect=lambda a, b: b)
299+
def test_tracked_backoff_generator_with_maximum(self, mock_uniform):
300+
"""
301+
Should cap the backoff at the maximum value
302+
"""
303+
generator = _helpers.TrackedBackoffGenerator(initial=1, multiplier=2, maximum=5)
304+
expected_values = [1, 2, 4, 5, 5, 5]
305+
for expected in expected_values:
306+
assert next(generator) == expected
307+
308+
def test_get_attempt_backoff_out_of_bounds(self):
309+
"""
310+
get_attempt_backoff should raise IndexError for out of bounds index
311+
"""
312+
generator = _helpers.TrackedBackoffGenerator()
313+
next(generator)
314+
next(generator)
315+
with pytest.raises(IndexError):
316+
generator.get_attempt_backoff(2)
317+
with pytest.raises(IndexError):
318+
generator.get_attempt_backoff(-3)

0 commit comments

Comments
 (0)