Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 9cadd63

Browse files
committed
don't check timeout on each yield by default
1 parent de07714 commit 9cadd63

File tree

4 files changed

+83
-45
lines changed

4 files changed

+83
-45
lines changed

google/api_core/retry_streaming.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
from typing import Callable, Optional, Iterable, Iterator, Generator, TypeVar, Any, cast
1818

19-
import datetime
2019
import logging
2120
import time
2221

@@ -104,6 +103,7 @@ def __init__(
104103
sleep_generator: Iterable[float],
105104
timeout: Optional[float] = None,
106105
on_error: Optional[Callable[[Exception], None]] = None,
106+
check_timeout_on_yield=False,
107107
):
108108
"""
109109
Args:
@@ -119,6 +119,11 @@ def __init__(
119119
on_error: A function to call while processing a
120120
retryable exception. Any error raised by this function will *not*
121121
be caught.
122+
check_timeout_on_yield: If True, the timeout value will be checked
123+
after each yield. If the timeout has been exceeded, the generator
124+
will raise a RetryError. Note that this adds an overhead to each
125+
yield, so it is preferred to add the timeout logic to the wrapped
126+
stream when possible.
122127
"""
123128
self.target_fn = target
124129
self.active_target: Iterator[T] = self.target_fn().__iter__()
@@ -130,6 +135,7 @@ def __init__(
130135
self.deadline = time.monotonic() + self.timeout
131136
else:
132137
self.deadline = None
138+
self._check_timeout_on_yield = check_timeout_on_yield
133139

134140
def __iter__(self) -> Generator[T, Any, None]:
135141
"""
@@ -194,7 +200,8 @@ def __next__(self) -> T:
194200
- the next value of the active_target iterator
195201
"""
196202
# check for expired timeouts before attempting to iterate
197-
self._check_timeout(time.monotonic())
203+
if self._check_timeout_on_yield:
204+
self._check_timeout(time.monotonic())
198205
try:
199206
return next(self.active_target)
200207
except Exception as exc:
@@ -233,7 +240,8 @@ def send(self, *args, **kwargs) -> T:
233240
- AttributeError if the active_target does not have a send() method
234241
"""
235242
# check for expired timeouts before attempting to iterate
236-
self._check_timeout(time.monotonic())
243+
if self._check_timeout_on_yield:
244+
self._check_timeout(time.monotonic())
237245
if getattr(self.active_target, "send", None):
238246
casted_target = cast(Generator, self.active_target)
239247
try:

google/api_core/retry_streaming_async.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ def __init__(
118118
sleep_generator: Iterable[float],
119119
timeout: Optional[float] = None,
120120
on_error: Optional[Callable[[Exception], None]] = None,
121+
check_timeout_on_yield=False,
121122
):
122123
"""
123124
Args:
@@ -133,6 +134,11 @@ def __init__(
133134
on_error: A function to call while processing a
134135
retryable exception. Any error raised by this function will *not*
135136
be caught.
137+
check_timeout_on_yield: If True, the timeout value will be checked
138+
after each yield. If the timeout has been exceeded, the generator
139+
will raise a RetryError. Note that this adds an overhead to each
140+
yield, so it is preferred to add the timeout logic to the wrapped
141+
stream when possible.
136142
"""
137143
self.target_fn = target
138144
# active target must be populated in an async context
@@ -146,6 +152,7 @@ def __init__(
146152
self.deadline = time.monotonic() + self.timeout
147153
else:
148154
self.deadline = None
155+
self._check_timeout_on_yield = check_timeout_on_yield
149156

150157
def _check_timeout(
151158
self, current_time: float, source_exception: Optional[Exception] = None
@@ -176,7 +183,7 @@ async def _ensure_active_target(self) -> AsyncIterator[T]:
176183
Returns:
177184
- The active_target iterable
178185
"""
179-
if not self.active_target:
186+
if self.active_target is None:
180187
new_iterable = self.target_fn()
181188
if isinstance(new_iterable, Awaitable):
182189
new_iterable = await new_iterable
@@ -226,7 +233,8 @@ async def _iteration_helper(self, iteration_routine: Awaitable) -> T:
226233
- The next value from the active_target iterator.
227234
"""
228235
# check for expired timeouts before attempting to iterate
229-
self._check_timeout(time.monotonic())
236+
if self._check_timeout_on_yield:
237+
self._check_timeout(time.monotonic())
230238
try:
231239
# grab the next value from the active_target
232240
# Note: here would be a good place to add a timeout, like asyncio.wait_for.

tests/asyncio/test_retry_async.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -746,15 +746,38 @@ async def __anext__(self):
746746
with pytest.raises(StopAsyncIteration):
747747
await retryable.__anext__()
748748

749+
@mock.patch("random.uniform", autospec=True, side_effect=lambda m, n: n)
750+
@mock.patch("asyncio.sleep", autospec=True)
749751
@pytest.mark.asyncio
750-
async def test_iterate_stream_after_deadline(self):
752+
async def test_yield_stream_after_deadline(self, sleep, uniform):
751753
"""
752-
Streaming retries should raise RetryError when calling next or send after deadline has passed
754+
By default, if the deadline is hit between yields, the generator will continue.
755+
756+
There is a flag that should cause the wrapper to test for the deadline after
757+
each yield.
753758
"""
754-
retry_ = retry_async.AsyncRetry(is_stream=True, deadline=0.01)
755-
decorated = retry_(self._generator_mock)
756-
generator = decorated(10)
757-
await generator.__anext__()
758-
await asyncio.sleep(0.02)
759-
with pytest.raises(exceptions.RetryError):
760-
await generator.__anext__()
759+
import time
760+
from google.api_core.retry_streaming_async import AsyncRetryableGenerator
761+
timeout = 2
762+
time_now = time.monotonic()
763+
now_patcher = mock.patch(
764+
"time.monotonic", return_value=time_now,
765+
)
766+
767+
with now_patcher as patched_now:
768+
no_check = AsyncRetryableGenerator(self._generator_mock, None, [], timeout=timeout, check_timeout_on_yield=False)
769+
assert no_check._check_timeout_on_yield is False
770+
check = AsyncRetryableGenerator(self._generator_mock, None, [], timeout=timeout, check_timeout_on_yield=True)
771+
assert check._check_timeout_on_yield is True
772+
773+
# first yield should be fine
774+
await check.__anext__()
775+
await no_check.__anext__()
776+
777+
# simulate a delay before next yield
778+
patched_now.return_value += timeout + 1
779+
780+
# second yield should raise when check_timeout_on_yield is True
781+
with pytest.raises(exceptions.RetryError):
782+
await check.__anext__()
783+
await no_check.__anext__()

tests/unit/test_retry.py

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -772,39 +772,38 @@ def test___call___with_is_stream(self, sleep):
772772
unpacked = [next(gen) for i in range(10)]
773773
assert unpacked == [0, 1, 2, 3, 4, 5, 0, 1, 2, 3]
774774

775-
def test_iterate_stream_after_deadline(self):
776-
"""
777-
Streaming retries should raise RetryError when calling next after deadline has passed
775+
@mock.patch("random.uniform", autospec=True, side_effect=lambda m, n: n)
776+
@mock.patch("asyncio.sleep", autospec=True)
777+
@pytest.mark.asyncio
778+
async def test_yield_stream_after_deadline(self, sleep, uniform):
778779
"""
779-
from time import sleep
780+
By default, if the deadline is hit between yields, the generator will continue.
780781
781-
retry_ = retry.Retry(
782-
predicate=retry.if_exception_type(ValueError),
783-
is_stream=True,
784-
deadline=0.01,
782+
There is a flag that should cause the wrapper to test for the deadline after
783+
each yield.
784+
"""
785+
import time
786+
from google.api_core.retry_streaming import RetryableGenerator
787+
timeout = 2
788+
time_now = time.monotonic()
789+
now_patcher = mock.patch(
790+
"time.monotonic", return_value=time_now,
785791
)
786-
decorated = retry_(self._generator_mock)
787-
generator = decorated(10)
788-
next(generator)
789-
sleep(0.02)
790-
with pytest.raises(exceptions.RetryError):
791-
next(generator)
792792

793-
def test_iterate_stream_send_after_deadline(self):
794-
"""
795-
Streaming retries should raise RetryError when calling send after deadline has passed
796-
"""
797-
from time import sleep
793+
with now_patcher as patched_now:
794+
no_check = RetryableGenerator(self._generator_mock, None, [], timeout=timeout, check_timeout_on_yield=False)
795+
assert no_check._check_timeout_on_yield is False
796+
check = RetryableGenerator(self._generator_mock, None, [], timeout=timeout, check_timeout_on_yield=True)
797+
assert check._check_timeout_on_yield is True
798798

799-
retry_ = retry.Retry(
800-
predicate=retry.if_exception_type(ValueError),
801-
is_stream=True,
802-
deadline=0.01,
803-
)
804-
decorated = retry_(self._generator_mock)
805-
generator = decorated(10)
806-
next(generator)
807-
generator.send("test")
808-
sleep(0.02)
809-
with pytest.raises(exceptions.RetryError):
810-
generator.send("test")
799+
# first yield should be fine
800+
next(check)
801+
next(no_check)
802+
803+
# simulate a delay before next yield
804+
patched_now.return_value += timeout + 1
805+
806+
# second yield should raise when check_timeout_on_yield is True
807+
with pytest.raises(exceptions.RetryError):
808+
next(check)
809+
next(no_check)

0 commit comments

Comments
 (0)