|
1 | 1 | import asyncio |
2 | 2 | import time |
3 | 3 | from functools import wraps |
4 | | -from random import randint |
5 | | -from typing import TYPE_CHECKING |
| 4 | +from inspect import iscoroutinefunction |
| 5 | +from random import random |
| 6 | +from typing import TYPE_CHECKING, overload |
6 | 7 |
|
7 | | -if TYPE_CHECKING: # pragma: no cover |
8 | | - from typing import Awaitable, Callable, Optional, Tuple, Type, TypeVar, Union |
| 8 | +if TYPE_CHECKING: |
| 9 | + import sys |
| 10 | + from collections.abc import Awaitable, Callable |
9 | 11 |
|
10 | | - from typing_extensions import ParamSpec |
| 12 | + if sys.version_info >= (3, 10): |
| 13 | + from typing import ParamSpec, TypeVar |
| 14 | + else: |
| 15 | + from typing import Type |
| 16 | + from typing_extensions import ParamSpec, TypeVar |
11 | 17 |
|
12 | | - P = ParamSpec("P") |
13 | | - T = TypeVar("T") |
| 18 | + type = Type |
| 19 | + |
| 20 | + ArgumentsType = ParamSpec("ArgumentsType") |
| 21 | + OuterReturnType = TypeVar("OuterReturnType") |
| 22 | + InnerReturnType = TypeVar("InnerReturnType") |
14 | 23 |
|
15 | 24 |
|
16 | 25 | def retry( |
17 | | - *ExceptionTypes: "Type[Exception]", tries: int = 3, delay: int = 1, backoff: int = 2 |
18 | | -) -> "Callable[[Callable[P, T]], Callable[P, T]]": |
| 26 | + *errors: "type[Exception]", |
| 27 | + count: int, |
| 28 | + wait: float, |
| 29 | + backoff: float, |
| 30 | + jitter: float, |
| 31 | +) -> "Callable[[Callable[ArgumentsType, OuterReturnType]], Callable[ArgumentsType, OuterReturnType]]": # noqa: E501 |
19 | 32 | """ |
20 | | - Retry with exponential backoff |
| 33 | + Decorate a function or coroutine to retry when it raises specified errors, |
| 34 | + apply exponential backoff and jitter to the wait time, |
| 35 | + and raise the last error if it retries too many times. |
21 | 36 |
|
22 | | - Original from: http://wiki.python.org/moin/PythonDecoratorLibrary#Retry |
| 37 | + Arguments: |
| 38 | + errors: Retry the function when it raises one of these errors. |
| 39 | + count: Retry the function this many times. |
| 40 | + wait: Wait this many seconds after the first error before retrying. |
| 41 | + backoff: Multiply the wait time by this amount for each additional error. |
| 42 | + jitter: Add a random amount of time (up to this percent as a decimal) |
| 43 | + to the wait time to prevent simultaneous retries. |
23 | 44 | """ |
24 | 45 |
|
25 | | - def retry_decorator(f: "Callable[P, T]") -> "Callable[P, T]": |
26 | | - @wraps(f) |
27 | | - def retry_fn(*args: "P.args", **kwargs: "P.kwargs") -> "T": |
28 | | - n_tries, n_delay = tries, delay |
29 | | - while n_tries > 1: |
30 | | - try: |
31 | | - return f(*args, **kwargs) |
32 | | - except ExceptionTypes: |
33 | | - time.sleep(n_delay) |
34 | | - n_tries -= 1 |
35 | | - n_delay *= backoff |
36 | | - return f(*args, **kwargs) |
37 | | - |
38 | | - return retry_fn |
| 46 | + def wait_time(times_retried: int) -> float: |
| 47 | + """ |
| 48 | + Calculate the sleep time based on number of times retried. |
| 49 | + """ |
| 50 | + return wait * backoff**times_retried * (1 + jitter * random()) |
39 | 51 |
|
40 | | - return retry_decorator |
| 52 | + @overload |
| 53 | + def retry_decorator( |
| 54 | + decorated: "Callable[ArgumentsType, Awaitable[InnerReturnType]]", |
| 55 | + ) -> "Callable[ArgumentsType, Awaitable[InnerReturnType]]": ... |
41 | 56 |
|
| 57 | + @overload |
| 58 | + def retry_decorator( |
| 59 | + decorated: "Callable[ArgumentsType, InnerReturnType]", |
| 60 | + ) -> "Callable[ArgumentsType, InnerReturnType]": ... |
42 | 61 |
|
43 | | -def aioretry( |
44 | | - *ExceptionTypes: "Type[Exception]", |
45 | | - tries: int = 3, |
46 | | - delay: "Union[int, Tuple[int, int]]" = 1, |
47 | | - backoff: int = 2, |
48 | | - condition: "Optional[Callable[[Exception], bool]]" = None, |
49 | | -) -> "Callable[[Callable[P, Awaitable[T]]], Callable[P, Awaitable[T]]]": |
50 | | - """ |
51 | | - Retry with exponential backoff |
52 | | -
|
53 | | - Original from: http://wiki.python.org/moin/PythonDecoratorLibrary#Retry |
54 | | - Options: |
55 | | - condition: Callable to evaluate if an exception of a given type |
56 | | - is retryable for additional handling |
57 | | - delay: an initial time to wait (seconds). If a tuple, choose a random number |
58 | | - in that range to start. This can helps prevent retries at the exact |
59 | | - same time across multiple concurrent function calls |
60 | | - """ |
| 62 | + def retry_decorator( |
| 63 | + decorated: "Callable[ArgumentsType, InnerReturnType]", |
| 64 | + ) -> "Callable[ArgumentsType, Awaitable[InnerReturnType]] | Callable[ArgumentsType, InnerReturnType]": # noqa: E501 |
| 65 | + """ |
| 66 | + Decorate either a function or coroutine as appropriate. |
| 67 | + """ |
| 68 | + if iscoroutinefunction(decorated): |
| 69 | + |
| 70 | + @wraps(decorated) |
| 71 | + async def retrying_coroutine( # type: ignore[return] |
| 72 | + *args: "ArgumentsType.args", **kwargs: "ArgumentsType.kwargs" |
| 73 | + ) -> "InnerReturnType": |
| 74 | + for times_retried in range(count + 1): |
| 75 | + try: |
| 76 | + return await decorated(*args, **kwargs) # type: ignore[no-any-return] |
| 77 | + except errors: |
| 78 | + if times_retried >= count: |
| 79 | + raise |
| 80 | + |
| 81 | + await asyncio.sleep(wait_time(times_retried)) |
| 82 | + |
| 83 | + return retrying_coroutine |
| 84 | + |
| 85 | + else: |
| 86 | + |
| 87 | + @wraps(decorated) |
| 88 | + def retrying_function( # type: ignore[return] |
| 89 | + *args: "ArgumentsType.args", **kwargs: "ArgumentsType.kwargs" |
| 90 | + ) -> "InnerReturnType": |
| 91 | + for times_retried in range(count + 1): |
| 92 | + try: |
| 93 | + return decorated(*args, **kwargs) |
| 94 | + except errors: |
| 95 | + if times_retried >= count: |
| 96 | + raise |
| 97 | + |
| 98 | + time.sleep(wait_time(times_retried)) |
61 | 99 |
|
62 | | - def retry_decorator(f: "Callable[P, Awaitable[T]]") -> "Callable[P, Awaitable[T]]": |
63 | | - @wraps(f) |
64 | | - async def retry_fn(*args: "P.args", **kwargs: "P.kwargs") -> "T": |
65 | | - n_tries = tries |
66 | | - if isinstance(delay, tuple): |
67 | | - # pick a random number to sleep |
68 | | - n_delay = randint(*delay) |
69 | | - else: |
70 | | - n_delay = delay |
71 | | - while True: |
72 | | - try: |
73 | | - return await f(*args, **kwargs) |
74 | | - except ExceptionTypes as e: |
75 | | - if condition and not condition(e): |
76 | | - raise |
77 | | - await asyncio.sleep(n_delay) |
78 | | - n_tries -= 1 |
79 | | - n_delay *= backoff |
80 | | - if n_tries <= 0: |
81 | | - raise |
82 | | - |
83 | | - return retry_fn |
| 100 | + return retrying_function |
84 | 101 |
|
85 | 102 | return retry_decorator |
0 commit comments