-
Notifications
You must be signed in to change notification settings - Fork 87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: retry and retry_async support streaming rpcs #495
Changes from 190 commits
953106a
89aeb75
27feb80
0dffa6d
b330c3b
67ceaa2
ee2647a
5a5396c
7afa76b
2d91ade
0cd384e
f72bbec
88eed5c
91f9cc4
c3eb997
f6c6201
57b0ee3
e814ce7
0ffb03f
a8024f3
c76f641
ee631e3
70eb78c
42ee132
102d83b
f029dbd
c83c62a
185826c
c5f7bbe
4242036
9c4799c
0bd6cab
67aeeaf
985b13a
0ea8297
b952652
6cb3e2d
99da116
7f862d0
04a4a69
d20cf08
183c221
d2217e4
d4a9d30
06d45cc
de41a14
dcb3766
dd368e4
452b9bb
6879418
847509f
b5e3796
7a7d9ac
6619895
27fc930
d6a23ea
90ef834
6201db6
61ce3a7
69149a1
d63871e
773e033
d1def5d
cbaaa1d
21a863f
878ddfb
7b0a600
0188228
902a4ab
74f3f3e
e506aad
5baa2aa
5c3805d
265d998
0423ebe
c4049f5
acd6546
b1ad4b3
8dcf67c
6104c59
43d0913
9ba7676
14c195c
de7b51a
4cdee6b
a526d65
ee2bbdd
5f82355
9900c40
2c2dcbe
3340399
de07714
67068ac
54325bc
bafa18b
2ae2a32
9cadd63
c9ef1d5
41c7868
30fccb9
a2b0e6c
4aa1ab4
8349424
ece5cf8
5ddda24
9e3ea92
3b06b3a
8bb6b0c
37c64a0
cee0028
3a7e5fa
ba6dc9f
0500b8b
1ccadb1
c312262
1fe57e0
4f09f29
06824b9
343157b
93f82cc
0915ca0
61e5ab5
51c125b
02604bc
6269db2
0dcd0de
54e9c81
2342910
eada0d7
ae2bf37
c8a4f26
2840b9f
82274a3
1594a17
9b0ddb0
8985127
60b20ab
237ca3d
a46c0f7
93727b7
796ae52
0688ffe
da048ab
80e5eb0
562079b
a0fecc5
8cc6ea9
e7a5cd4
02c12cc
03b1608
b05b11f
0b5d3a2
03f2af5
5fee888
239ed7d
94eb0f5
7d1e246
b0faa2d
6c44298
51df672
e207376
39716a7
2bbf33f
3b03bfa
e63701d
c101ea6
3642d74
34cfa08
583181d
b311b87
19a998d
5637e88
c4be5f2
4d9e762
2e9e84b
d183a7e
e2d9c9c
4543106
d791aad
638cc68
f7b1e14
07db4c2
d448a52
781426a
4a05404
b221c8d
b5b4534
0f1145d
8408512
aa69c56
d1ac29d
3ab88fc
382d0e2
4258823
1bc9731
aafe057
8095229
de9f518
7864667
4c24322
7855513
f4bfb02
a88cf6f
b5c62e1
852f4f8
cd8323e
ace61eb
1bbd1f0
35cc00a
89abfa4
74ab817
85b3e02
6dbe17d
71e5888
cbae3d3
61198b8
acf9752
7cf9fbf
f62439a
b7abeca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# Copyright 2017 Google LLC | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
"""Retry implementation for Google API client libraries.""" | ||
|
||
from .retry_base import exponential_sleep_generator | ||
from .retry_base import if_exception_type | ||
from .retry_base import if_transient_error | ||
from .retry_base import _build_retry_error | ||
from .retry_base import RetryFailureReason | ||
from .retry_unary import Retry | ||
from .retry_unary import retry_target | ||
from .retry_unary_async import AsyncRetry | ||
from .retry_unary_async import retry_target as retry_target_async | ||
from .retry_streaming import StreamingRetry | ||
from .retry_streaming import retry_target_stream | ||
from .retry_streaming_async import AsyncStreamingRetry | ||
from .retry_streaming_async import retry_target_stream as retry_target_stream_async | ||
|
||
__all__ = ( | ||
"exponential_sleep_generator", | ||
"if_exception_type", | ||
"if_transient_error", | ||
"_build_retry_error", | ||
"RetryFailureReason", | ||
"Retry", | ||
"AsyncRetry", | ||
"StreamingRetry", | ||
"AsyncStreamingRetry", | ||
"retry_target", | ||
"retry_target_async", | ||
"retry_target_stream", | ||
"retry_target_stream_async", | ||
) |
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,346 @@ | ||||||||||||||
# Copyright 2017 Google LLC | ||||||||||||||
# | ||||||||||||||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||||||||||||||
# you may not use this file except in compliance with the License. | ||||||||||||||
# You may obtain a copy of the License at | ||||||||||||||
# | ||||||||||||||
# http://www.apache.org/licenses/LICENSE-2.0 | ||||||||||||||
# | ||||||||||||||
# Unless required by applicable law or agreed to in writing, software | ||||||||||||||
# distributed under the License is distributed on an "AS IS" BASIS, | ||||||||||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||||||||||
# See the License for the specific language governing permissions and | ||||||||||||||
# limitations under the License. | ||||||||||||||
|
||||||||||||||
"""Shared classes and functions for retrying requests. | ||||||||||||||
|
||||||||||||||
:class:`_BaseRetry` is the base class for :class:`Retry`, | ||||||||||||||
:class:`AsyncRetry`, :class:`StreamingRetry`, and :class:`AsyncStreamingRetry`. | ||||||||||||||
""" | ||||||||||||||
|
||||||||||||||
from __future__ import annotations | ||||||||||||||
|
||||||||||||||
import logging | ||||||||||||||
import random | ||||||||||||||
import time | ||||||||||||||
|
||||||||||||||
from enum import Enum | ||||||||||||||
from typing import Any, Callable, TYPE_CHECKING | ||||||||||||||
|
||||||||||||||
import requests.exceptions | ||||||||||||||
|
||||||||||||||
from google.api_core import exceptions | ||||||||||||||
from google.auth import exceptions as auth_exceptions | ||||||||||||||
|
||||||||||||||
if TYPE_CHECKING: | ||||||||||||||
import sys | ||||||||||||||
|
||||||||||||||
if sys.version_info >= (3, 11): | ||||||||||||||
from typing import Self | ||||||||||||||
else: | ||||||||||||||
from typing_extensions import Self | ||||||||||||||
|
||||||||||||||
_DEFAULT_INITIAL_DELAY = 1.0 # seconds | ||||||||||||||
_DEFAULT_MAXIMUM_DELAY = 60.0 # seconds | ||||||||||||||
_DEFAULT_DELAY_MULTIPLIER = 2.0 | ||||||||||||||
_DEFAULT_DEADLINE = 60.0 * 2.0 # seconds | ||||||||||||||
|
||||||||||||||
_LOGGER = logging.getLogger("google.api_core.retry") | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
def if_exception_type( | ||||||||||||||
*exception_types: type[Exception], | ||||||||||||||
) -> Callable[[Exception], bool]: | ||||||||||||||
"""Creates a predicate to check if the exception is of a given type. | ||||||||||||||
|
||||||||||||||
Args: | ||||||||||||||
exception_types (Sequence[:func:`type`]): The exception types to check | ||||||||||||||
for. | ||||||||||||||
|
||||||||||||||
Returns: | ||||||||||||||
Callable[Exception]: A predicate that returns True if the provided | ||||||||||||||
exception is of the given type(s). | ||||||||||||||
""" | ||||||||||||||
|
||||||||||||||
def if_exception_type_predicate(exception: Exception) -> bool: | ||||||||||||||
"""Bound predicate for checking an exception type.""" | ||||||||||||||
return isinstance(exception, exception_types) | ||||||||||||||
|
||||||||||||||
return if_exception_type_predicate | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
# pylint: disable=invalid-name | ||||||||||||||
# Pylint sees this as a constant, but it is also an alias that should be | ||||||||||||||
# considered a function. | ||||||||||||||
if_transient_error = if_exception_type( | ||||||||||||||
exceptions.InternalServerError, | ||||||||||||||
exceptions.TooManyRequests, | ||||||||||||||
exceptions.ServiceUnavailable, | ||||||||||||||
requests.exceptions.ConnectionError, | ||||||||||||||
requests.exceptions.ChunkedEncodingError, | ||||||||||||||
auth_exceptions.TransportError, | ||||||||||||||
) | ||||||||||||||
"""A predicate that checks if an exception is a transient API error. | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. doc: I'm fine with the function-type naming you have, but if Python treats this as a constant, we should make sure the documentation format is appropriate for that so it shows up in IDEs, etc. Is the triple-quoted form the right format for constants' comments? I only recall the hash-prefixed form. It seems triple-quoted strings get assigned to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not new code, I just moved the existing function to this new shared file If we do changes here, I'd probably prefer to open an issue and address it separately, since it's not related to streaming retries. Let me know what you think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, sure. Could you file that issue? Thanks! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Opened here: #569 |
||||||||||||||
|
||||||||||||||
The following server errors are considered transient: | ||||||||||||||
|
||||||||||||||
- :class:`google.api_core.exceptions.InternalServerError` - HTTP 500, gRPC | ||||||||||||||
``INTERNAL(13)`` and its subclasses. | ||||||||||||||
- :class:`google.api_core.exceptions.TooManyRequests` - HTTP 429 | ||||||||||||||
- :class:`google.api_core.exceptions.ServiceUnavailable` - HTTP 503 | ||||||||||||||
- :class:`requests.exceptions.ConnectionError` | ||||||||||||||
- :class:`requests.exceptions.ChunkedEncodingError` - The server declared | ||||||||||||||
chunked encoding but sent an invalid chunk. | ||||||||||||||
- :class:`google.auth.exceptions.TransportError` - Used to indicate an | ||||||||||||||
error occurred during an HTTP request. | ||||||||||||||
""" | ||||||||||||||
# pylint: enable=invalid-name | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
def exponential_sleep_generator( | ||||||||||||||
initial: float, maximum: float, multiplier: float = _DEFAULT_DELAY_MULTIPLIER | ||||||||||||||
): | ||||||||||||||
"""Generates sleep intervals based on the exponential back-off algorithm. | ||||||||||||||
|
||||||||||||||
This implements the `Truncated Exponential Back-off`_ algorithm. | ||||||||||||||
|
||||||||||||||
.. _Truncated Exponential Back-off: | ||||||||||||||
https://cloud.google.com/storage/docs/exponential-backoff | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. doc: This link only calls it "exponential backoff" (without "truncation" in the title). It links to the Wikipedia page, which does mention "truncated exponential backoff". However, what you're doing with the use of
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is also existing code moved over. So I'd prefer to open an issue for this instead of address it here if that's ok FWIW, I've also heard this called "full jitter", and that seems to get a lot of google hits There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, sure. Could you file that issue? Thanks! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||
|
||||||||||||||
Args: | ||||||||||||||
initial (float): The minimum amount of time to delay. This must | ||||||||||||||
be greater than 0. | ||||||||||||||
maximum (float): The maximum amount of time to delay. | ||||||||||||||
multiplier (float): The multiplier applied to the delay. | ||||||||||||||
|
||||||||||||||
Yields: | ||||||||||||||
float: successive sleep intervals. | ||||||||||||||
""" | ||||||||||||||
delay = min(initial, maximum) | ||||||||||||||
vchudnov-g marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
while True: | ||||||||||||||
yield random.uniform(0.0, delay) | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tangent (NO-OP): Discussion for another day: I wonder whether it would be more useful to set the lower limit to some non-zero value, like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah this code pre-dates the PR, and it seems like it was written to align with this retryPolicy spec |
||||||||||||||
delay = min(delay * multiplier, maximum) | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
class RetryFailureReason(Enum): | ||||||||||||||
""" | ||||||||||||||
The cause of a failed retry, used when building exceptions | ||||||||||||||
""" | ||||||||||||||
|
||||||||||||||
TIMEOUT = 0 | ||||||||||||||
NON_RETRYABLE_ERROR = 1 | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
def _build_retry_error( | ||||||||||||||
exc_list: list[Exception], | ||||||||||||||
reason: RetryFailureReason, | ||||||||||||||
timeout_val: float | None, | ||||||||||||||
**kwargs: Any, | ||||||||||||||
) -> tuple[Exception, Exception | None]: | ||||||||||||||
""" | ||||||||||||||
Default exception_factory implementation. Builds an exception after the retry fails | ||||||||||||||
daniel-sanche marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
|
||||||||||||||
Args: | ||||||||||||||
- exc_list: list of exceptions that occurred during the retry | ||||||||||||||
- reason: reason for the retry failure. | ||||||||||||||
Can be TIMEOUT or NON_RETRYABLE_ERROR | ||||||||||||||
- timeout_val: the original timeout value for the retry, for use in the exception message | ||||||||||||||
daniel-sanche marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
|
||||||||||||||
Returns: | ||||||||||||||
- tuple: a tuple of the exception to be raised, and the cause exception if any | ||||||||||||||
""" | ||||||||||||||
if reason == RetryFailureReason.TIMEOUT: | ||||||||||||||
# return RetryError with the most recent exception as the cause | ||||||||||||||
src_exc = exc_list[-1] if exc_list else None | ||||||||||||||
timeout_val_str = f"of {timeout_val:0.1f}s " if timeout_val is not None else "" | ||||||||||||||
return ( | ||||||||||||||
exceptions.RetryError( | ||||||||||||||
f"Timeout {timeout_val_str}exceeded", | ||||||||||||||
src_exc, | ||||||||||||||
), | ||||||||||||||
src_exc, | ||||||||||||||
) | ||||||||||||||
elif exc_list: | ||||||||||||||
# return most recent exception encountered | ||||||||||||||
return exc_list[-1], None | ||||||||||||||
else: | ||||||||||||||
# no exceptions were given in exc_list. Raise generic RetryError | ||||||||||||||
return exceptions.RetryError("Unknown error", None), None | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
def _retry_error_helper( | ||||||||||||||
exc: Exception, | ||||||||||||||
deadline: float | None, | ||||||||||||||
next_sleep: float, | ||||||||||||||
error_list: list[Exception], | ||||||||||||||
predicate_fn: Callable[[Exception], bool], | ||||||||||||||
on_error_fn: Callable[[Exception], None] | None, | ||||||||||||||
exc_factory_fn: Callable[ | ||||||||||||||
[list[Exception], RetryFailureReason], | ||||||||||||||
tuple[Exception, Exception | None], | ||||||||||||||
vchudnov-g marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
], | ||||||||||||||
): | ||||||||||||||
""" | ||||||||||||||
Shared logic for handling an error for all retry implementations | ||||||||||||||
|
||||||||||||||
- Raises an error on timeout or non-retryable error | ||||||||||||||
- Calls on_error_fn if provided | ||||||||||||||
- Logs the error | ||||||||||||||
|
||||||||||||||
Args: | ||||||||||||||
- exc: the exception that was raised | ||||||||||||||
- deadline: the deadline for the retry, calculated as a diff from time.monotonic() | ||||||||||||||
- next_sleep: the calculated next sleep interval | ||||||||||||||
- error_list: the list of exceptions that have been raised so far | ||||||||||||||
- predicate_fn: the predicate that was used to determine if the exception should be retried | ||||||||||||||
- on_error_fn: the callback that was called when the exception was raised | ||||||||||||||
- exc_factory_fn: the callback that was called to build the exception to be raised on terminal failure | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure about the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I see your point, but then we're not accurately describing how this function uses the preidcate. If a non- There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah that works for me, I changed the text |
||||||||||||||
""" | ||||||||||||||
error_list.append(exc) | ||||||||||||||
if not predicate_fn(exc): | ||||||||||||||
final_exc, source_exc = exc_factory_fn( | ||||||||||||||
error_list, | ||||||||||||||
RetryFailureReason.NON_RETRYABLE_ERROR, | ||||||||||||||
) | ||||||||||||||
raise final_exc from source_exc | ||||||||||||||
if on_error_fn is not None: | ||||||||||||||
on_error_fn(exc) | ||||||||||||||
if deadline is not None and time.monotonic() + next_sleep > deadline: | ||||||||||||||
final_exc, source_exc = exc_factory_fn( | ||||||||||||||
error_list, | ||||||||||||||
RetryFailureReason.TIMEOUT, | ||||||||||||||
) | ||||||||||||||
raise final_exc from source_exc | ||||||||||||||
_LOGGER.debug( | ||||||||||||||
"Retrying due to {}, sleeping {:.1f}s ...".format(error_list[-1], next_sleep) | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
class _BaseRetry(object): | ||||||||||||||
""" | ||||||||||||||
Base class for retry configuration objects. This class is intended to capture retry | ||||||||||||||
and backoff configuration that is common to both synchronous and asynchronous retries, | ||||||||||||||
for both unary and streaming RPCs. It is not intended to be instantiated directly, | ||||||||||||||
but rather to be subclassed by the various retry configuration classes. | ||||||||||||||
""" | ||||||||||||||
|
||||||||||||||
def __init__( | ||||||||||||||
self, | ||||||||||||||
predicate: Callable[[Exception], bool] = if_transient_error, | ||||||||||||||
vchudnov-g marked this conversation as resolved.
Show resolved
Hide resolved
daniel-sanche marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
initial: float = _DEFAULT_INITIAL_DELAY, | ||||||||||||||
maximum: float = _DEFAULT_MAXIMUM_DELAY, | ||||||||||||||
multiplier: float = _DEFAULT_DELAY_MULTIPLIER, | ||||||||||||||
timeout: float = _DEFAULT_DEADLINE, | ||||||||||||||
on_error: Callable[[Exception], Any] | None = None, | ||||||||||||||
**kwargs: Any, | ||||||||||||||
) -> None: | ||||||||||||||
self._predicate = predicate | ||||||||||||||
self._initial = initial | ||||||||||||||
self._multiplier = multiplier | ||||||||||||||
self._maximum = maximum | ||||||||||||||
self._timeout = kwargs.get("deadline", timeout) | ||||||||||||||
self._deadline = self._timeout | ||||||||||||||
self._on_error = on_error | ||||||||||||||
|
||||||||||||||
def __call__(self, *args, **kwargs) -> Any: | ||||||||||||||
raise NotImplementedError("Not implemented in base class") | ||||||||||||||
|
||||||||||||||
@property | ||||||||||||||
def deadline(self) -> float | None: | ||||||||||||||
""" | ||||||||||||||
DEPRECATED: use ``timeout`` instead. Refer to the ``Retry`` class | ||||||||||||||
documentation for details. | ||||||||||||||
""" | ||||||||||||||
return self._timeout | ||||||||||||||
|
||||||||||||||
@property | ||||||||||||||
def timeout(self) -> float | None: | ||||||||||||||
return self._timeout | ||||||||||||||
|
||||||||||||||
def _replace( | ||||||||||||||
self, | ||||||||||||||
predicate: Callable[[Exception], bool] | None = None, | ||||||||||||||
initial: float | None = None, | ||||||||||||||
maximum: float | None = None, | ||||||||||||||
multiplier: float | None = None, | ||||||||||||||
timeout: float | None = None, | ||||||||||||||
on_error: Callable[[Exception], Any] | None = None, | ||||||||||||||
) -> Self: | ||||||||||||||
return type(self)( | ||||||||||||||
predicate=predicate or self._predicate, | ||||||||||||||
initial=initial or self._initial, | ||||||||||||||
maximum=maximum or self._maximum, | ||||||||||||||
multiplier=multiplier or self._multiplier, | ||||||||||||||
timeout=timeout or self._timeout, | ||||||||||||||
on_error=on_error or self._on_error, | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
def with_deadline(self, deadline: float | None) -> Self: | ||||||||||||||
"""Return a copy of this retry with the given timeout. | ||||||||||||||
|
||||||||||||||
DEPRECATED: use :meth:`with_timeout` instead. Refer to the ``Retry`` class | ||||||||||||||
documentation for details. | ||||||||||||||
|
||||||||||||||
Args: | ||||||||||||||
deadline (float): How long to keep retrying in seconds. | ||||||||||||||
daniel-sanche marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
|
||||||||||||||
Returns: | ||||||||||||||
Retry: A new retry instance with the given timeout. | ||||||||||||||
""" | ||||||||||||||
return self._replace(timeout=deadline) | ||||||||||||||
|
||||||||||||||
def with_timeout(self, timeout: float) -> Self: | ||||||||||||||
"""Return a copy of this retry with the given timeout. | ||||||||||||||
|
||||||||||||||
Args: | ||||||||||||||
timeout (float): How long to keep retrying, in seconds. | ||||||||||||||
|
||||||||||||||
Returns: | ||||||||||||||
Retry: A new retry instance with the given timeout. | ||||||||||||||
""" | ||||||||||||||
return self._replace(timeout=timeout) | ||||||||||||||
|
||||||||||||||
def with_predicate(self, predicate: Callable[[Exception], bool]) -> Self: | ||||||||||||||
"""Return a copy of this retry with the given predicate. | ||||||||||||||
|
||||||||||||||
Args: | ||||||||||||||
predicate (Callable[Exception]): A callable that should return | ||||||||||||||
``True`` if the given exception is retryable. | ||||||||||||||
|
||||||||||||||
Returns: | ||||||||||||||
Retry: A new retry instance with the given predicate. | ||||||||||||||
""" | ||||||||||||||
return self._replace(predicate=predicate) | ||||||||||||||
|
||||||||||||||
def with_delay( | ||||||||||||||
self, | ||||||||||||||
initial: float | None = None, | ||||||||||||||
maximum: float | None = None, | ||||||||||||||
multiplier: float | None = None, | ||||||||||||||
) -> Self: | ||||||||||||||
"""Return a copy of this retry with the given delay options. | ||||||||||||||
|
||||||||||||||
Args: | ||||||||||||||
initial (float): The minimum amount of time to delay. This must | ||||||||||||||
be greater than 0. | ||||||||||||||
maximum (float): The maximum amount of time to delay. | ||||||||||||||
daniel-sanche marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
multiplier (float): The multiplier applied to the delay. | ||||||||||||||
|
||||||||||||||
Returns: | ||||||||||||||
Retry: A new retry instance with the given predicate. | ||||||||||||||
""" | ||||||||||||||
return self._replace(initial=initial, maximum=maximum, multiplier=multiplier) | ||||||||||||||
|
||||||||||||||
def __str__(self) -> str: | ||||||||||||||
return ( | ||||||||||||||
"<{} predicate={}, initial={:.1f}, maximum={:.1f}, " | ||||||||||||||
"multiplier={:.1f}, timeout={}, on_error={}>".format( | ||||||||||||||
type(self).__name__, | ||||||||||||||
self._predicate, | ||||||||||||||
self._initial, | ||||||||||||||
self._maximum, | ||||||||||||||
self._multiplier, | ||||||||||||||
self._timeout, # timeout can be None, thus no {:.1f} | ||||||||||||||
self._on_error, | ||||||||||||||
) | ||||||||||||||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're exposing this, should we remove the leading private underscore?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good point, removed