python-backoff 3.0.0__tar.gz → 3.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_backoff-3.0.0 → python_backoff-3.1.0}/PKG-INFO +10 -4
- {python_backoff-3.0.0 → python_backoff-3.1.0}/README.rst +6 -2
- {python_backoff-3.0.0 → python_backoff-3.1.0}/backoff/__init__.py +6 -5
- {python_backoff-3.0.0 → python_backoff-3.1.0}/backoff/_common.py +5 -7
- {python_backoff-3.0.0 → python_backoff-3.1.0}/backoff/_decorator.py +16 -6
- {python_backoff-3.0.0 → python_backoff-3.1.0}/backoff/_wait_gen.py +77 -24
- python_backoff-3.1.0/docs/advanced/combining-decorators.md +123 -0
- python_backoff-3.1.0/docs/advanced/custom-strategies.md +144 -0
- python_backoff-3.1.0/docs/advanced/runtime-config.md +102 -0
- python_backoff-3.1.0/docs/api/backoff/index.md +1 -0
- python_backoff-3.1.0/docs/api/summary.md +1 -0
- python_backoff-3.1.0/docs/changelog.md +1 -0
- python_backoff-3.1.0/docs/examples.md +492 -0
- python_backoff-3.1.0/docs/faq.md +454 -0
- python_backoff-3.1.0/docs/getting-started.md +206 -0
- python_backoff-3.1.0/docs/index.md +73 -0
- python_backoff-3.1.0/docs/user-guide/async.md +194 -0
- python_backoff-3.1.0/docs/user-guide/configuration.md +397 -0
- python_backoff-3.1.0/docs/user-guide/context-manager.md +63 -0
- python_backoff-3.1.0/docs/user-guide/decorators.md +248 -0
- python_backoff-3.1.0/docs/user-guide/event-handlers.md +435 -0
- python_backoff-3.1.0/docs/user-guide/logging.md +234 -0
- python_backoff-3.1.0/docs/user-guide/wait-strategies.md +333 -0
- {python_backoff-3.0.0 → python_backoff-3.1.0}/pyproject.toml +10 -7
- {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/common.py +1 -1
- {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/test_backoff.py +51 -32
- {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/test_backoff_async.py +76 -55
- {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/test_integration.py +12 -9
- {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/test_retry_context.py +8 -8
- {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/test_wait_gen.py +39 -0
- {python_backoff-3.0.0 → python_backoff-3.1.0}/.gitignore +0 -0
- {python_backoff-3.0.0 → python_backoff-3.1.0}/LICENSE +0 -0
- {python_backoff-3.0.0 → python_backoff-3.1.0}/backoff/_async.py +0 -0
- {python_backoff-3.0.0 → python_backoff-3.1.0}/backoff/_jitter.py +0 -0
- {python_backoff-3.0.0 → python_backoff-3.1.0}/backoff/_sync.py +0 -0
- {python_backoff-3.0.0 → python_backoff-3.1.0}/backoff/_typing.py +0 -0
- {python_backoff-3.0.0 → python_backoff-3.1.0}/backoff/py.typed +0 -0
- {python_backoff-3.0.0 → python_backoff-3.1.0}/backoff/types.py +0 -0
- {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/__init__.py +0 -0
- {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/conftest.py +0 -0
- {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/test_jitter.py +0 -0
- {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/test_package.py +0 -0
- {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/test_types.py +0 -0
- {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/typing/__init__.py +0 -0
- {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/typing/basic.py +0 -0
- {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/typing/decorators.py +0 -0
- {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/typing/pyrefly.py +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-backoff
|
|
3
|
-
Version: 3.
|
|
4
|
-
|
|
3
|
+
Version: 3.1.0
|
|
4
|
+
Import-Name: backoff
|
|
5
|
+
Summary: Function decoration and context managers for backoff and retry
|
|
5
6
|
Project-URL: Documentation, https://python-backoff.github.io/backoff
|
|
6
7
|
Project-URL: Repository, https://github.com/python-backoff/backoff
|
|
7
8
|
Author-email: Bob Green <rgreen@aquent.com>
|
|
@@ -21,6 +22,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
21
22
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
23
|
Classifier: Programming Language :: Python :: 3.14
|
|
23
24
|
Classifier: Programming Language :: Python :: 3.15
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.16
|
|
24
26
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
25
27
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
28
|
Classifier: Topic :: Utilities
|
|
@@ -39,9 +41,11 @@ backoff
|
|
|
39
41
|
.. image:: https://img.shields.io/github/license/python-backoff/backoff
|
|
40
42
|
:target: https://github.com/python-backoff/backoff/blob/main/LICENSE
|
|
41
43
|
|
|
44
|
+
*Note: this is an actively maintained fork of https://github.com/litl/backoff.*
|
|
45
|
+
|
|
42
46
|
**Function decoration for backoff and retry**
|
|
43
47
|
|
|
44
|
-
This
|
|
48
|
+
This library provides function decorators which can be used to wrap a
|
|
45
49
|
function such that it will be retried until some condition is met. It
|
|
46
50
|
is meant to be of use when accessing unreliable resources with the
|
|
47
51
|
potential for intermittent failures i.e. network resources and external
|
|
@@ -52,6 +56,8 @@ Decorators support both regular functions for synchronous code and
|
|
|
52
56
|
`asyncio <https://docs.python.org/3/library/asyncio.html>`__'s coroutines
|
|
53
57
|
for asynchronous code.
|
|
54
58
|
|
|
59
|
+
Context managers are also available to enable retries of arbitrary code blocks.
|
|
60
|
+
|
|
55
61
|
Examples
|
|
56
62
|
========
|
|
57
63
|
|
|
@@ -182,7 +188,7 @@ The ``on_predicate`` decorator is used to retry when a particular
|
|
|
182
188
|
condition is true of the return value of the target function. This may
|
|
183
189
|
be useful when polling a resource for externally generated content.
|
|
184
190
|
|
|
185
|
-
Here's an example which uses a
|
|
191
|
+
Here's an example which uses a Fibonacci sequence backoff when the
|
|
186
192
|
return value of the target function is the empty list:
|
|
187
193
|
|
|
188
194
|
.. code-block:: python
|
|
@@ -10,9 +10,11 @@ backoff
|
|
|
10
10
|
.. image:: https://img.shields.io/github/license/python-backoff/backoff
|
|
11
11
|
:target: https://github.com/python-backoff/backoff/blob/main/LICENSE
|
|
12
12
|
|
|
13
|
+
*Note: this is an actively maintained fork of https://github.com/litl/backoff.*
|
|
14
|
+
|
|
13
15
|
**Function decoration for backoff and retry**
|
|
14
16
|
|
|
15
|
-
This
|
|
17
|
+
This library provides function decorators which can be used to wrap a
|
|
16
18
|
function such that it will be retried until some condition is met. It
|
|
17
19
|
is meant to be of use when accessing unreliable resources with the
|
|
18
20
|
potential for intermittent failures i.e. network resources and external
|
|
@@ -23,6 +25,8 @@ Decorators support both regular functions for synchronous code and
|
|
|
23
25
|
`asyncio <https://docs.python.org/3/library/asyncio.html>`__'s coroutines
|
|
24
26
|
for asynchronous code.
|
|
25
27
|
|
|
28
|
+
Context managers are also available to enable retries of arbitrary code blocks.
|
|
29
|
+
|
|
26
30
|
Examples
|
|
27
31
|
========
|
|
28
32
|
|
|
@@ -153,7 +157,7 @@ The ``on_predicate`` decorator is used to retry when a particular
|
|
|
153
157
|
condition is true of the return value of the target function. This may
|
|
154
158
|
be useful when polling a resource for externally generated content.
|
|
155
159
|
|
|
156
|
-
Here's an example which uses a
|
|
160
|
+
Here's an example which uses a Fibonacci sequence backoff when the
|
|
157
161
|
return value of the target function is the empty list:
|
|
158
162
|
|
|
159
163
|
.. code-block:: python
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Function decoration for backoff and retry
|
|
2
|
+
Function decoration for backoff and retry.
|
|
3
3
|
|
|
4
4
|
This module provides function decorators which can be used to wrap a
|
|
5
5
|
function such that it will be retried until some condition is met. It
|
|
@@ -8,18 +8,19 @@ potential for intermittent failures i.e. network resources and external
|
|
|
8
8
|
APIs. Somewhat more generally, it may also be of use for dynamically
|
|
9
9
|
polling resources for externally generated content.
|
|
10
10
|
|
|
11
|
-
For examples and full documentation see
|
|
12
|
-
https://
|
|
11
|
+
For examples and full documentation, see
|
|
12
|
+
[the official documentation](https://backoff.readthedocs.io/en/latest/).
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
from backoff._common import Attempt
|
|
16
16
|
from backoff._decorator import aretry_context, on_exception, on_predicate, retry_context
|
|
17
17
|
from backoff._jitter import full_jitter, random_jitter
|
|
18
|
-
from backoff._wait_gen import constant, decay, expo, fibo, runtime
|
|
18
|
+
from backoff._wait_gen import capped, constant, decay, expo, fibo, runtime
|
|
19
19
|
|
|
20
20
|
__all__ = [
|
|
21
21
|
"Attempt",
|
|
22
22
|
"aretry_context",
|
|
23
|
+
"capped",
|
|
23
24
|
"constant",
|
|
24
25
|
"decay",
|
|
25
26
|
"expo",
|
|
@@ -32,4 +33,4 @@ __all__ = [
|
|
|
32
33
|
"runtime",
|
|
33
34
|
]
|
|
34
35
|
|
|
35
|
-
__version__ = "3.
|
|
36
|
+
__version__ = "3.1.0"
|
|
@@ -43,7 +43,7 @@ _HandlerT = TypeVar("_HandlerT")
|
|
|
43
43
|
def _maybe_call(f: _MaybeCallable[T] | None, *args: Any, **kwargs: Any) -> T | None:
|
|
44
44
|
if callable(f):
|
|
45
45
|
try:
|
|
46
|
-
return f(*args, **kwargs) # ty:ignore[
|
|
46
|
+
return f(*args, **kwargs) # ty:ignore[invalid-return-type]
|
|
47
47
|
except TypeError:
|
|
48
48
|
return f # type: ignore[return-value] # ty:ignore[invalid-return-type]
|
|
49
49
|
else:
|
|
@@ -212,10 +212,9 @@ def _config_handlers(
|
|
|
212
212
|
# Default backoff handler
|
|
213
213
|
def _log_backoff(details: Details, logger: _AnyLogger, log_level: int) -> None:
|
|
214
214
|
msg = "Backing off %s(...) for %.1fs (%s)"
|
|
215
|
-
log_args = [details["target"].__name__, details["wait"]] # ty:ignore[
|
|
215
|
+
log_args = [details["target"].__name__, details["wait"]] # ty:ignore[missing-attribute]
|
|
216
216
|
|
|
217
|
-
exc
|
|
218
|
-
if exc is not None:
|
|
217
|
+
if (exc := details.get("exception")) is not None:
|
|
219
218
|
exc_fmt = traceback.format_exception_only(type(exc), exc)[-1]
|
|
220
219
|
log_args.append(exc_fmt.rstrip("\n"))
|
|
221
220
|
else:
|
|
@@ -226,10 +225,9 @@ def _log_backoff(details: Details, logger: _AnyLogger, log_level: int) -> None:
|
|
|
226
225
|
# Default giveup handler
|
|
227
226
|
def _log_giveup(details: Details, logger: _AnyLogger, log_level: int) -> None:
|
|
228
227
|
msg = "Giving up %s(...) after %d tries (%s)"
|
|
229
|
-
log_args = [details["target"].__name__, details["tries"]] # ty:ignore[
|
|
228
|
+
log_args = [details["target"].__name__, details["tries"]] # ty:ignore[missing-attribute]
|
|
230
229
|
|
|
231
|
-
exc
|
|
232
|
-
if exc is not None:
|
|
230
|
+
if (exc := details.get("exception")) is not None:
|
|
233
231
|
exc_fmt = traceback.format_exception_only(type(exc), exc)[-1]
|
|
234
232
|
log_args.append(exc_fmt.rstrip("\n"))
|
|
235
233
|
else:
|
|
@@ -307,9 +307,14 @@ def retry_context(
|
|
|
307
307
|
Unlike `on_exception`, this doesn't wrap a whole function; it lets a
|
|
308
308
|
caller retry an arbitrary block of code:
|
|
309
309
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
310
|
+
```python
|
|
311
|
+
for attempt in backoff.retry_context(
|
|
312
|
+
ValueError,
|
|
313
|
+
backoff.expo,
|
|
314
|
+
):
|
|
315
|
+
with attempt:
|
|
316
|
+
do_something()
|
|
317
|
+
```
|
|
313
318
|
|
|
314
319
|
Each `attempt` is a context manager: exceptions matching `exception`
|
|
315
320
|
are caught, and the loop either sleeps and retries or lets the
|
|
@@ -403,9 +408,14 @@ def aretry_context(
|
|
|
403
408
|
) -> AsyncGenerator[Attempt, None]:
|
|
404
409
|
"""Async counterpart to `retry_context`, for use with `async for`.
|
|
405
410
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
411
|
+
```python
|
|
412
|
+
async for attempt in backoff.aretry_context(
|
|
413
|
+
ValueError,
|
|
414
|
+
backoff.expo,
|
|
415
|
+
):
|
|
416
|
+
with attempt:
|
|
417
|
+
await do_something()
|
|
418
|
+
```
|
|
409
419
|
|
|
410
420
|
`giveup` and the handlers may be sync or async callables. See
|
|
411
421
|
`retry_context` for the full argument reference.
|
|
@@ -2,11 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import itertools
|
|
4
4
|
import math
|
|
5
|
-
from typing import TYPE_CHECKING, Any
|
|
5
|
+
from typing import TYPE_CHECKING, Any, ParamSpec
|
|
6
6
|
|
|
7
7
|
if TYPE_CHECKING:
|
|
8
8
|
from collections.abc import Callable, Generator, Iterable
|
|
9
9
|
|
|
10
|
+
P = ParamSpec("P")
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
def expo(
|
|
12
14
|
base: float = 2,
|
|
@@ -38,7 +40,7 @@ def decay(
|
|
|
38
40
|
decay_factor: float = 1,
|
|
39
41
|
min_value: float | None = None,
|
|
40
42
|
) -> Generator[float, Any, None]:
|
|
41
|
-
"""Generator for exponential decay
|
|
43
|
+
"""Generator for [exponential decay](https://en.wikipedia.org/wiki/Exponential_decay).
|
|
42
44
|
|
|
43
45
|
Args:
|
|
44
46
|
initial_value: initial quantity
|
|
@@ -46,8 +48,6 @@ def decay(
|
|
|
46
48
|
min_value: The minimum value to yield. Once the value in the
|
|
47
49
|
true exponential sequence is lower than this, the value
|
|
48
50
|
of min_value will forever after be yielded.
|
|
49
|
-
|
|
50
|
-
[1] https://en.wikipedia.org/wiki/Exponential_decay
|
|
51
51
|
"""
|
|
52
52
|
# Advance past initial .send() call
|
|
53
53
|
yield 0
|
|
@@ -61,12 +61,12 @@ def decay(
|
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
def fibo(max_value: int | None = None) -> Generator[int, Any, None]:
|
|
64
|
-
"""Generator for
|
|
64
|
+
"""Generator for Fibonacci decay.
|
|
65
65
|
|
|
66
66
|
Args:
|
|
67
67
|
max_value: The maximum value to yield. Once the value in the
|
|
68
|
-
true
|
|
69
|
-
of max_value will forever after be yielded.
|
|
68
|
+
true Fibonacci sequence exceeds this, the value
|
|
69
|
+
of `max_value` will forever after be yielded.
|
|
70
70
|
"""
|
|
71
71
|
# Advance past initial .send() call
|
|
72
72
|
yield 0
|
|
@@ -99,6 +99,57 @@ def constant(interval: float | Iterable[float] = 1) -> Generator[float, Any, Non
|
|
|
99
99
|
yield val
|
|
100
100
|
|
|
101
101
|
|
|
102
|
+
def capped(
|
|
103
|
+
wait_gen: Callable[P, Generator[float, Any, None]],
|
|
104
|
+
*,
|
|
105
|
+
min_value: float | None = None,
|
|
106
|
+
max_value: float | None = None,
|
|
107
|
+
) -> Callable[P, Generator[float, Any, None]]:
|
|
108
|
+
"""Wraps a wait generator, clamping each value it yields.
|
|
109
|
+
|
|
110
|
+
Useful for wait generators without their own bound, such as
|
|
111
|
+
[`constant`][backoff.constant] or [`runtime`][backoff.runtime] (e.g.
|
|
112
|
+
capping a server-provided `Retry-After` value so a misbehaving server
|
|
113
|
+
can't stall retries indefinitely):
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
backoff.on_predicate(
|
|
117
|
+
backoff.capped(backoff.runtime, max_value=60),
|
|
118
|
+
predicate=lambda r: r.status_code == 429,
|
|
119
|
+
value=lambda r: int(r.headers.get("Retry-After", 1)),
|
|
120
|
+
)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
wait_gen: The wait generator to wrap.
|
|
125
|
+
min_value: The minimum value to yield. Values below this are
|
|
126
|
+
raised to min_value.
|
|
127
|
+
max_value: The maximum value to yield. Values above this are
|
|
128
|
+
lowered to max_value.
|
|
129
|
+
"""
|
|
130
|
+
if min_value is not None and max_value is not None and min_value > max_value:
|
|
131
|
+
msg = f"`min_value` {min_value} cannot be greater than `max_value` {max_value}"
|
|
132
|
+
raise ValueError(msg)
|
|
133
|
+
|
|
134
|
+
def generator(*args: P.args, **kwargs: P.kwargs) -> Generator[float, Any, None]:
|
|
135
|
+
gen = wait_gen(*args, **kwargs)
|
|
136
|
+
gen.send(None)
|
|
137
|
+
|
|
138
|
+
send_value = yield 0
|
|
139
|
+
while True:
|
|
140
|
+
try:
|
|
141
|
+
value = gen.send(send_value)
|
|
142
|
+
except StopIteration:
|
|
143
|
+
return
|
|
144
|
+
if max_value is not None:
|
|
145
|
+
value = min(value, max_value)
|
|
146
|
+
if min_value is not None:
|
|
147
|
+
value = max(value, min_value)
|
|
148
|
+
send_value = yield value
|
|
149
|
+
|
|
150
|
+
return generator
|
|
151
|
+
|
|
152
|
+
|
|
102
153
|
def runtime(*, value: Callable[[Any], float]) -> Generator[float, Any, None]:
|
|
103
154
|
"""Generator that is based on parsing the return value or thrown
|
|
104
155
|
exception of the decorated method
|
|
@@ -106,23 +157,25 @@ def runtime(*, value: Callable[[Any], float]) -> Generator[float, Any, None]:
|
|
|
106
157
|
Useful for honoring a server-specified retry delay, e.g. an HTTP
|
|
107
158
|
`Retry-After` header, rather than a fixed wait sequence:
|
|
108
159
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
160
|
+
```python
|
|
161
|
+
# with on_predicate, `value` receives the return value
|
|
162
|
+
@backoff.on_predicate(
|
|
163
|
+
backoff.runtime,
|
|
164
|
+
predicate=lambda r: r.status_code == 429,
|
|
165
|
+
value=lambda r: int(r.headers.get("Retry-After", 1)),
|
|
166
|
+
)
|
|
167
|
+
def get_page():
|
|
168
|
+
return requests.get(url)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# with on_exception, `value` receives the raised exception
|
|
172
|
+
@backoff.on_exception(
|
|
173
|
+
backoff.runtime,
|
|
174
|
+
RetryableError,
|
|
175
|
+
value=lambda e: e.wait_seconds,
|
|
176
|
+
)
|
|
177
|
+
def get_page(): ...
|
|
178
|
+
```
|
|
126
179
|
|
|
127
180
|
Args:
|
|
128
181
|
value: a callable which takes as input the decorated
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Combining Decorators
|
|
2
|
+
|
|
3
|
+
Stack multiple backoff decorators for complex retry logic.
|
|
4
|
+
|
|
5
|
+
## Basics
|
|
6
|
+
|
|
7
|
+
Decorators are applied from bottom to top (inside out):
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
@backoff.on_predicate(backoff.fibo, lambda x: x is None) # Applied last
|
|
11
|
+
@backoff.on_exception(backoff.expo, HTTPError) # Applied second
|
|
12
|
+
@backoff.on_exception(backoff.expo, Timeout) # Applied first
|
|
13
|
+
def complex_operation():
|
|
14
|
+
pass
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Common Patterns
|
|
18
|
+
|
|
19
|
+
### Different Exceptions, Different Strategies
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
@backoff.on_exception(
|
|
23
|
+
backoff.expo,
|
|
24
|
+
requests.exceptions.Timeout,
|
|
25
|
+
max_time=300, # Generous timeout for network issues
|
|
26
|
+
)
|
|
27
|
+
@backoff.on_exception(
|
|
28
|
+
backoff.expo,
|
|
29
|
+
requests.exceptions.HTTPError,
|
|
30
|
+
max_time=60, # Shorter timeout for HTTP errors
|
|
31
|
+
giveup=lambda e: 400 <= e.response.status_code < 500,
|
|
32
|
+
)
|
|
33
|
+
def api_call(url):
|
|
34
|
+
response = requests.get(url)
|
|
35
|
+
response.raise_for_status()
|
|
36
|
+
return response.json()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Exception + Predicate
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
@backoff.on_predicate(
|
|
43
|
+
backoff.constant,
|
|
44
|
+
lambda result: result.get("status") == "pending",
|
|
45
|
+
interval=5,
|
|
46
|
+
max_time=600,
|
|
47
|
+
)
|
|
48
|
+
@backoff.on_exception(
|
|
49
|
+
backoff.expo,
|
|
50
|
+
requests.exceptions.RequestException,
|
|
51
|
+
max_time=60,
|
|
52
|
+
)
|
|
53
|
+
def poll_until_ready(job_id):
|
|
54
|
+
response = requests.get(f"/api/jobs/{job_id}")
|
|
55
|
+
response.raise_for_status()
|
|
56
|
+
return response.json()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Execution Order
|
|
60
|
+
|
|
61
|
+
Inner decorators execute first:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
calls = []
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def track_call(func_name):
|
|
68
|
+
def handler(details):
|
|
69
|
+
calls.append(func_name)
|
|
70
|
+
|
|
71
|
+
return handler
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@backoff.on_exception(
|
|
75
|
+
backoff.constant,
|
|
76
|
+
ValueError,
|
|
77
|
+
on_backoff=track_call("outer"),
|
|
78
|
+
max_tries=2,
|
|
79
|
+
interval=0.01,
|
|
80
|
+
)
|
|
81
|
+
@backoff.on_exception(
|
|
82
|
+
backoff.constant,
|
|
83
|
+
TypeError,
|
|
84
|
+
on_backoff=track_call("inner"),
|
|
85
|
+
max_tries=2,
|
|
86
|
+
interval=0.01,
|
|
87
|
+
)
|
|
88
|
+
def failing_function(error_type):
|
|
89
|
+
raise error_type("Test")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- If `TypeError` raised: inner decorator retries
|
|
93
|
+
- If `ValueError` raised: outer decorator retries
|
|
94
|
+
- Both errors: inner handles TypeError, then outer handles ValueError
|
|
95
|
+
|
|
96
|
+
## Best Practices
|
|
97
|
+
|
|
98
|
+
### Specific Before General
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
@backoff.on_exception(backoff.expo, Exception) # Catch-all
|
|
102
|
+
@backoff.on_exception(backoff.fibo, ConnectionError) # Specific
|
|
103
|
+
def network_operation():
|
|
104
|
+
pass
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Short Timeouts Inside, Long Outside
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
@backoff.on_exception(
|
|
111
|
+
backoff.expo,
|
|
112
|
+
Exception,
|
|
113
|
+
max_time=600, # Overall 10-minute limit
|
|
114
|
+
)
|
|
115
|
+
@backoff.on_exception(
|
|
116
|
+
backoff.constant,
|
|
117
|
+
Timeout,
|
|
118
|
+
interval=1,
|
|
119
|
+
max_tries=3, # Quick retries for timeouts
|
|
120
|
+
)
|
|
121
|
+
def layered_retry():
|
|
122
|
+
pass
|
|
123
|
+
```
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Custom Wait Strategies
|
|
2
|
+
|
|
3
|
+
Create custom wait generators for specialized retry patterns.
|
|
4
|
+
|
|
5
|
+
## Wait Generator Interface
|
|
6
|
+
|
|
7
|
+
A wait generator is a function that yields wait times in seconds:
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
def my_wait_gen():
|
|
11
|
+
"""Yields: 1, 2, 3, 4, 5, 5, 5, ..."""
|
|
12
|
+
# Note: Using `yield from` can be unsafe in backoff wait generators.
|
|
13
|
+
for i in range(1, 6):
|
|
14
|
+
yield i
|
|
15
|
+
while True:
|
|
16
|
+
yield 5
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@backoff.on_exception(my_wait_gen, Exception)
|
|
20
|
+
def my_function():
|
|
21
|
+
pass
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Parameters
|
|
25
|
+
|
|
26
|
+
Accept parameters to customize behavior:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
def linear_backoff(start=1, increment=1, max_value=None):
|
|
30
|
+
"""Linear backoff: start, start+increment, start+2*increment, ..."""
|
|
31
|
+
value = start
|
|
32
|
+
while True:
|
|
33
|
+
if max_value and value > max_value:
|
|
34
|
+
yield max_value
|
|
35
|
+
else:
|
|
36
|
+
yield value
|
|
37
|
+
value += increment
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@backoff.on_exception(
|
|
41
|
+
linear_backoff,
|
|
42
|
+
Exception,
|
|
43
|
+
start=2,
|
|
44
|
+
increment=3,
|
|
45
|
+
max_value=30,
|
|
46
|
+
)
|
|
47
|
+
def my_function():
|
|
48
|
+
pass
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Examples
|
|
52
|
+
|
|
53
|
+
### Polynomial Backoff
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
def polynomial_backoff(base=2, exponent=2, max_value=None):
|
|
57
|
+
"""Polynomial: base^(tries^exponent)"""
|
|
58
|
+
n = 1
|
|
59
|
+
while True:
|
|
60
|
+
value = base ** (n**exponent)
|
|
61
|
+
if max_value and value > max_value:
|
|
62
|
+
yield max_value
|
|
63
|
+
else:
|
|
64
|
+
yield value
|
|
65
|
+
n += 1
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@backoff.on_exception(
|
|
69
|
+
polynomial_backoff,
|
|
70
|
+
Exception,
|
|
71
|
+
base=2,
|
|
72
|
+
exponent=1.5,
|
|
73
|
+
)
|
|
74
|
+
def my_function():
|
|
75
|
+
pass
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Stepped Backoff
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
def stepped_backoff(steps):
|
|
82
|
+
"""Different wait times for different ranges
|
|
83
|
+
steps = [(3, 1), (5, 5), (None, 10)] # 3 tries at 1s, next 5 at 5s, rest at 10s
|
|
84
|
+
"""
|
|
85
|
+
for max_tries, wait_time in steps:
|
|
86
|
+
if max_tries is None:
|
|
87
|
+
while True:
|
|
88
|
+
yield wait_time
|
|
89
|
+
else:
|
|
90
|
+
for _ in range(max_tries):
|
|
91
|
+
yield wait_time
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@backoff.on_exception(
|
|
95
|
+
stepped_backoff,
|
|
96
|
+
Exception,
|
|
97
|
+
steps=[(3, 1), (3, 5), (None, 30)],
|
|
98
|
+
)
|
|
99
|
+
def my_function():
|
|
100
|
+
pass
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Random Backoff
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import random
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def random_backoff(min_wait=1, max_wait=60):
|
|
110
|
+
"""Random wait between min and max"""
|
|
111
|
+
while True:
|
|
112
|
+
yield random.uniform(min_wait, max_wait)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@backoff.on_exception(
|
|
116
|
+
random_backoff,
|
|
117
|
+
Exception,
|
|
118
|
+
min_wait=1,
|
|
119
|
+
max_wait=10,
|
|
120
|
+
)
|
|
121
|
+
def my_function():
|
|
122
|
+
pass
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Time-of-Day Aware
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from datetime import datetime, timezone
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def business_hours_backoff():
|
|
132
|
+
"""Shorter waits during business hours"""
|
|
133
|
+
while True:
|
|
134
|
+
hour = datetime.now(tz=timezone.utc).hour
|
|
135
|
+
if 9 <= hour < 17:
|
|
136
|
+
yield 5 # 5 seconds during business hours
|
|
137
|
+
else:
|
|
138
|
+
yield 60 # 1 minute otherwise
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@backoff.on_exception(business_hours_backoff, Exception)
|
|
142
|
+
def my_function():
|
|
143
|
+
pass
|
|
144
|
+
```
|