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

Commit 00d70db

Browse files
committed
pulled backoff generator into PR
1 parent 2ef2c3c commit 00d70db

2 files changed

Lines changed: 81 additions & 0 deletions

File tree

google/cloud/bigtable/data/_helpers.py

Lines changed: 20 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

@@ -97,6 +98,25 @@ def _attempt_timeout_generator(
9798
yield max(0, min(per_request_timeout, deadline - time.monotonic()))
9899

99100

101+
def backoff_generator(initial=0.01, multiplier=2, maximum=60):
102+
"""
103+
Build a generator for exponential backoff sleep times.
104+
105+
This implementation builds on top of api_core.retries.exponential_sleep_generator,
106+
adding the ability to retrieve previous values using the send(idx) method. This is
107+
used by the Metrics class to track the sleep times used for each attempt.
108+
"""
109+
history = []
110+
subgenerator = exponential_sleep_generator(initial, multiplier, maximum)
111+
while True:
112+
next_backoff = next(subgenerator)
113+
history.append(next_backoff)
114+
sent_idx = yield next_backoff
115+
while sent_idx is not None:
116+
# requesting from history
117+
sent_idx = yield history[sent_idx]
118+
119+
100120
def _retry_exception_factory(
101121
exc_list: list[Exception],
102122
reason: RetryFailureReason,

tests/unit/data/test__helpers.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,67 @@ def test_attempt_timeout_w_sleeps(self):
9999
expected_value -= sleep_time
100100

101101

102+
class TestBackoffGenerator:
103+
"""
104+
test backoff_generator wrapper.
105+
Should wrap api_core.exponential_sleep_generator, with added history
106+
"""
107+
108+
def test_defaults(self):
109+
"""
110+
expect defaults: initial=0.01, multiplier=2, maximum=60
111+
"""
112+
with mock.patch(
113+
"google.cloud.bigtable.data._helpers.exponential_sleep_generator"
114+
) as mock_exponential_sleep_generator:
115+
generator = _helpers.backoff_generator()
116+
next(generator)
117+
assert mock_exponential_sleep_generator.call_args[0] == (0.01, 2, 60)
118+
119+
def test_wraps_exponential_sleep_generator(self):
120+
"""test that it wraps exponential_sleep_generator"""
121+
args = (1, 2, 3)
122+
with mock.patch(
123+
"google.cloud.bigtable.data._helpers.exponential_sleep_generator"
124+
) as mock_exponential_sleep_generator:
125+
expected_results = [1, 7, 9, "a", "b"]
126+
mock_exponential_sleep_generator.return_value = iter(expected_results)
127+
generator = _helpers.backoff_generator(*args)
128+
for val in expected_results:
129+
assert next(generator) == val
130+
assert mock_exponential_sleep_generator.call_count == 1
131+
# args from backoff generator should be passed through
132+
assert mock_exponential_sleep_generator.call_args == mock.call(*args)
133+
134+
def test_send_gives_history(self):
135+
"""
136+
Calling send with an index should give back the value that was yeilded at that index
137+
"""
138+
with mock.patch(
139+
"google.cloud.bigtable.data._helpers.exponential_sleep_generator"
140+
) as mock_exponential_sleep_generator:
141+
expected_results = [2, 4, 6, 8, 10]
142+
mock_exponential_sleep_generator.return_value = iter(expected_results)
143+
generator = _helpers.backoff_generator()
144+
# calling next should send values from wrapped iterator
145+
assert next(generator) == 2
146+
assert next(generator) == 4
147+
assert next(generator) == 6
148+
# calling send with an index should return the value at that index
149+
assert generator.send(0) == expected_results[0]
150+
assert generator.send(2) == expected_results[2]
151+
assert generator.send(1) == expected_results[1]
152+
assert generator.send(0) == expected_results[0]
153+
assert generator.send(0) == expected_results[0]
154+
# should be able to continue iterating as normal
155+
assert next(generator) == 8
156+
assert generator.send(0) == expected_results[0]
157+
assert next(generator) == 10
158+
# calling an index out of range should raise an error
159+
with pytest.raises(IndexError):
160+
generator.send(100)
161+
162+
102163
class TestValidateTimeouts:
103164
def test_validate_timeouts_error_messages(self):
104165
with pytest.raises(ValueError) as e:

0 commit comments

Comments
 (0)