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

Commit e0c1b45

Browse files
committed
feat: retry generates backoff value after completing on_error callbacks
1 parent 7fbd5fd commit e0c1b45

5 files changed

Lines changed: 28 additions & 27 deletions

File tree

google/api_core/retry/retry_base.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import time
2626

2727
from enum import Enum
28-
from typing import Any, Callable, Optional, TYPE_CHECKING
28+
from typing import Any, Callable, Optional, Iterable, TYPE_CHECKING
2929

3030
import requests.exceptions
3131

@@ -174,7 +174,7 @@ def build_retry_error(
174174
def _retry_error_helper(
175175
exc: Exception,
176176
deadline: float | None,
177-
next_sleep: float,
177+
sleep_generator: Iterable[float],
178178
error_list: list[Exception],
179179
predicate_fn: Callable[[Exception], bool],
180180
on_error_fn: Callable[[Exception], None] | None,
@@ -183,7 +183,7 @@ def _retry_error_helper(
183183
tuple[Exception, Exception | None],
184184
],
185185
original_timeout: float | None,
186-
):
186+
) -> float:
187187
"""
188188
Shared logic for handling an error for all retry implementations
189189
@@ -194,13 +194,15 @@ def _retry_error_helper(
194194
Args:
195195
- exc: the exception that was raised
196196
- deadline: the deadline for the retry, calculated as a diff from time.monotonic()
197-
- next_sleep: the next sleep interval
197+
- sleep_generator: iterable to draw the next backoff value from
198198
- error_list: the list of exceptions that have been raised so far
199199
- predicate_fn: takes `exc` and returns true if the operation should be retried
200200
- on_error_fn: callback to execute when a retryable error occurs
201201
- exc_factory_fn: callback used to build the exception to be raised on terminal failure
202202
- original_timeout_val: the original timeout value for the retry (in seconds),
203203
to be passed to the exception factory for building an error message
204+
Returns:
205+
- the next backoff value to use
204206
"""
205207
error_list.append(exc)
206208
if not predicate_fn(exc):
@@ -212,6 +214,10 @@ def _retry_error_helper(
212214
raise final_exc from source_exc
213215
if on_error_fn is not None:
214216
on_error_fn(exc)
217+
try:
218+
next_sleep = next(sleep_generator)
219+
except StopIteration:
220+
raise ValueError("Sleep generator stopped yielding sleep values.") from exc
215221
if deadline is not None and time.monotonic() + next_sleep > deadline:
216222
final_exc, source_exc = exc_factory_fn(
217223
error_list,
@@ -222,6 +228,7 @@ def _retry_error_helper(
222228
_LOGGER.debug(
223229
"Retrying due to {}, sleeping {:.1f}s ...".format(error_list[-1], next_sleep)
224230
)
231+
return next_sleep
225232

226233

227234
class _BaseRetry(object):

google/api_core/retry/retry_streaming.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def retry_target_stream(
108108
)
109109
error_list: list[Exception] = []
110110

111-
for sleep in sleep_generator:
111+
while True:
112112
# Start a new retry loop
113113
try:
114114
# Note: in the future, we can add a ResumptionStrategy object
@@ -121,20 +121,18 @@ def retry_target_stream(
121121
# This function explicitly must deal with broad exceptions.
122122
except Exception as exc:
123123
# defer to shared logic for handling errors
124-
_retry_error_helper(
124+
next_sleep = _retry_error_helper(
125125
exc,
126126
deadline,
127-
sleep,
127+
sleep_generator,
128128
error_list,
129129
predicate,
130130
on_error,
131131
exception_factory,
132132
timeout,
133133
)
134134
# if exception not raised, sleep before next attempt
135-
time.sleep(sleep)
136-
137-
raise ValueError("Sleep generator stopped yielding sleep values.")
135+
time.sleep(next_sleep)
138136

139137

140138
class StreamingRetry(_BaseRetry):

google/api_core/retry/retry_streaming_async.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ async def retry_target_stream(
111111
error_list: list[Exception] = []
112112
target_is_generator: bool | None = None
113113

114-
for sleep in sleep_generator:
114+
while True:
115115
# Start a new retry loop
116116
try:
117117
# Note: in the future, we can add a ResumptionStrategy object
@@ -174,22 +174,22 @@ async def retry_target_stream(
174174
# This function explicitly must deal with broad exceptions.
175175
except Exception as exc:
176176
# defer to shared logic for handling errors
177-
_retry_error_helper(
177+
next_sleep = _retry_error_helper(
178178
exc,
179179
deadline,
180-
sleep,
180+
sleep_generator,
181181
error_list,
182182
predicate,
183183
on_error,
184184
exception_factory,
185185
timeout,
186186
)
187187
# if exception not raised, sleep before next attempt
188-
await asyncio.sleep(sleep)
188+
await asyncio.sleep(next_sleep)
189+
189190
finally:
190191
if target_is_generator and target_iterator is not None:
191192
await cast(AsyncGenerator["_Y", None], target_iterator).aclose()
192-
raise ValueError("Sleep generator stopped yielding sleep values.")
193193

194194

195195
class AsyncStreamingRetry(_BaseRetry):

google/api_core/retry/retry_unary.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def retry_target(
139139
deadline = time.monotonic() + timeout if timeout is not None else None
140140
error_list: list[Exception] = []
141141

142-
for sleep in sleep_generator:
142+
while True:
143143
try:
144144
result = target()
145145
if inspect.isawaitable(result):
@@ -150,20 +150,18 @@ def retry_target(
150150
# This function explicitly must deal with broad exceptions.
151151
except Exception as exc:
152152
# defer to shared logic for handling errors
153-
_retry_error_helper(
153+
next_sleep = _retry_error_helper(
154154
exc,
155155
deadline,
156-
sleep,
156+
sleep_generator,
157157
error_list,
158158
predicate,
159159
on_error,
160160
exception_factory,
161161
timeout,
162162
)
163163
# if exception not raised, sleep before next attempt
164-
time.sleep(sleep)
165-
166-
raise ValueError("Sleep generator stopped yielding sleep values.")
164+
time.sleep(next_sleep)
167165

168166

169167
class Retry(_BaseRetry):

google/api_core/retry/retry_unary_async.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,27 +150,25 @@ async def retry_target(
150150
deadline = time.monotonic() + timeout if timeout is not None else None
151151
error_list: list[Exception] = []
152152

153-
for sleep in sleep_generator:
153+
while True:
154154
try:
155155
return await target()
156156
# pylint: disable=broad-except
157157
# This function explicitly must deal with broad exceptions.
158158
except Exception as exc:
159159
# defer to shared logic for handling errors
160-
_retry_error_helper(
160+
next_sleep = _retry_error_helper(
161161
exc,
162162
deadline,
163-
sleep,
163+
sleep_generator,
164164
error_list,
165165
predicate,
166166
on_error,
167167
exception_factory,
168168
timeout,
169169
)
170170
# if exception not raised, sleep before next attempt
171-
await asyncio.sleep(sleep)
172-
173-
raise ValueError("Sleep generator stopped yielding sleep values.")
171+
await asyncio.sleep(next_sleep)
174172

175173

176174
class AsyncRetry(_BaseRetry):

0 commit comments

Comments
 (0)