New upstream release.
Kali Janitor
2 years ago
0 | [report] | |
1 | show_missing = True | |
2 | ||
3 | # Regexes for lines to exclude from consideration | |
4 | exclude_lines = | |
5 | # Have to re-enable the standard pragma | |
6 | pragma: no cover | |
7 | pragma: python=3\.5 |
0 | 0 | language: python |
1 | 1 | python: |
2 | - "2.7" | |
3 | - "3.5" | |
4 | - "3.6" | |
5 | 2 | - "3.7" |
6 | 3 | - "3.8" |
7 | 4 | - "3.9" |
8 | matrix: | |
9 | include: | |
10 | - python: "3.5" | |
11 | env: PYTHONASYNCIODEBUG=x | |
12 | - python: "3.6" | |
13 | env: PYTHONASYNCIODEBUG=x | |
14 | - python: "3.7" | |
15 | env: PYTHONASYNCIODEBUG=x | |
16 | - python: "3.8" | |
17 | env: PYTHONASYNCIODEBUG=x | |
18 | - python: "3.9" | |
19 | env: PYTHONASYNCIODEBUG=x | |
20 | ||
5 | - "3.10" | |
21 | 6 | before_install: |
22 | 7 | - pip install poetry more-itertools |
23 | 8 | install: |
0 | # Change Log | |
0 | # Changelog | |
1 | 1 | |
2 | ## [v1.0.3] - 2014-06-05 | |
2 | ## [v2.0.1] - 2022-04-27 | |
3 | 3 | ### Changed |
4 | - Make logging unicode safe | |
5 | - Log on_predicate backoff as INFO rather than ERROR | |
4 | - Allow None for jitter keyword arg (typing) | |
6 | 5 | |
7 | ## [v1.0.4] - 2014-08-12 | |
6 | ||
7 | ## [v2.0.0] - 2022-04-26 | |
8 | 8 | ### Added |
9 | - Python 2.6 support from @Bonko | |
10 | - Python 3.0 support from @robyoung | |
11 | - Run tests in Travis from @robyoung | |
9 | - Add raise_on_giveup keyword arg for decorators | |
10 | - Add backoff.runtime wait generator for dynamically setting wait times based | |
11 | on target function return value or exception details | |
12 | ### Changed | |
13 | - Improve type hints for on_success, on_backoff, on_giveup handlers | |
14 | - Use decorator-specific detail and handler type hints | |
15 | - Optionally use typing_extensions for python 3.7 type hinting | |
16 | - Drop python 3.6 support | |
17 | - Add python 3.10 support | |
12 | 18 | |
13 | ## [v1.0.5] - 2015-02-03 | |
19 | ## [v1.11.1] - 2021-07-14 | |
20 | ### Fixed | |
21 | - Update __version__ in backoff module | |
22 | ||
23 | ## [v1.11.0] - 2021-07-12 | |
24 | ### Added | |
25 | - Configurable logging levels for backoff and giveup events | |
14 | 26 | ### Changed |
15 | - Add a default interval of 1 second for the constant generator | |
16 | - Improve on_predicate stop condition avoiding extra sleep | |
27 | - Minor documentation fixes | |
17 | 28 | |
18 | ## [v1.0.6] - 2015-02-10 | |
29 | ## [v1.10.0] - 2019-12-07 | |
30 | ### Changed | |
31 | - Allow sync decorator call from async function | |
32 | - NOTE: THIS WILL BE THE FINAL PYTHON 2.7 COMPATIBLE RELEASE. | |
33 | ||
34 | ## [v1.9.2] - 2019-11-19 | |
35 | ### Changed | |
36 | - Don't include tests and changelog in distribution | |
37 | ||
38 | ## [v1.9.1] - 2019-11-18 | |
39 | ### Changed | |
40 | - Include tests and changelog in distribution | |
41 | ||
42 | ## [v1.9.0] - 2019-11-16 | |
43 | ### Changed | |
44 | - Support python 3.8 | |
45 | ||
46 | ## [v1.8.1] - 2019-10-11 | |
47 | ### Changed | |
48 | - Use arguments in log messages rather than fully formatting log | |
49 | https://github.com/litl/backoff/pull/82 from @lbernick | |
50 | ||
51 | ## [v1.8.0] - 2018-12-20 | |
19 | 52 | ### Added |
20 | - Coveralls.io integration from @singingwolfboy | |
53 | - Custom loggers | |
54 | - Iterable intervals for constant wait_gen for predefined wait sequences | |
55 | ### Changed | |
56 | - Give up on StopIteration raised in wait generators | |
57 | - Nullary jitter signature deprecation warning | |
21 | 58 | |
59 | ## [v1.7.0] - 2018-11-23 | |
22 | 60 | ### Changed |
23 | - Fix logging bug for function calls with tuple params | |
61 | - Support Python 3.7 | |
62 | - Drop support for async in Python 3.4 | |
63 | - Drop support for Python 2.6 | |
64 | - Update development dependencies | |
65 | - Use poetry for dependencies and packaging | |
24 | 66 | |
25 | ## [v1.0.7] - 2015-02-10 | |
67 | ## [v1.6.0] - 2018-07-14 | |
68 | ### Changed | |
69 | - Change default log level from ERROR to INFO | |
70 | - Log retries on exception as INFO | |
26 | 71 | |
72 | ## [v1.5.0] - 2018-04-11 | |
73 | ### Added | |
74 | - Add max_time keyword argument | |
75 | ||
76 | ## [v1.4.3] - 2017-05-22 | |
27 | 77 | ### Changed |
28 | - Fix string formatting for python 2.6 | |
78 | - Add license to source distribution | |
29 | 79 | |
30 | ## [v1.1.0] - 2015-12-08 | |
80 | ## [v1.4.2] - 2017-04-25 | |
81 | ### Changed | |
82 | - Use documented logger name https://github.com/litl/backoff/pull/32 | |
83 | from @pquentin | |
84 | ||
85 | ## [v1.4.1] - 2017-04-21 | |
31 | 86 | ### Added |
32 | - Event handling for success, backoff, and giveup | |
33 | - Change log | |
87 | - Expose __version__ at package root | |
88 | ### Changed | |
89 | - Fix checking for running sync version in coroutine in case when event | |
90 | loop is not set from @rutsky | |
34 | 91 | |
92 | ## [v1.4.0] - 2017-02-05 | |
93 | ### Added | |
94 | - Async support via `asyncio` coroutines (Python 3.4) from @rutsky | |
35 | 95 | ### Changed |
36 | - Docs and test for multi exception invocations | |
37 | - Update dev environment test dependencies | |
96 | - Refactor `backoff` module into package with identical API | |
97 | ||
98 | ## [v1.3.2] - 2016-11-18 | |
99 | ### Changed | |
100 | - Don't log retried args and kwargs by default | |
101 | - README.rst syntax highlighting from @dethi | |
102 | ||
103 | ## [v1.3.1] - 2016-08-08 | |
104 | ### Changed | |
105 | - Include README.rst in source distribution (fixes package) | |
106 | ||
107 | ## [v1.3.0] - 2016-08-08 | |
108 | ### Added | |
109 | - Support runtime configuration with optional callable kwargs | |
110 | - Add giveup kwarg for exception inspection | |
111 | ### Changed | |
112 | - Documentation fixes | |
113 | ||
114 | ## [v1.2.1] - 2016-05-27 | |
115 | ### Changed | |
116 | - Documentation fixes | |
117 | ||
38 | 118 | |
39 | 119 | ## [v1.2.0] - 2016-05-26 |
40 | 120 | ### Added |
45 | 125 | - Change README to reST for the benefit of pypi :( |
46 | 126 | - Remove docstring doc generation and make README canonical |
47 | 127 | |
48 | ## [v1.2.1] - 2016-05-27 | |
128 | ## [v1.1.0] - 2015-12-08 | |
129 | ### Added | |
130 | - Event handling for success, backoff, and giveup | |
131 | - Change log | |
49 | 132 | ### Changed |
50 | - Documentation fixes | |
133 | - Docs and test for multi exception invocations | |
134 | - Update dev environment test dependencies | |
51 | 135 | |
52 | ## [v1.3.0] - 2016-08-08 | |
136 | ## [v1.0.7] - 2015-02-10 | |
137 | ### Changed | |
138 | - Fix string formatting for python 2.6 | |
139 | ||
140 | ## [v1.0.6] - 2015-02-10 | |
53 | 141 | ### Added |
54 | - Support runtime configuration with optional callable kwargs | |
55 | - Add giveup kwarg for exception inspection | |
142 | - Coveralls.io integration from @singingwolfboy | |
143 | ### Changed | |
144 | - Fix logging bug for function calls with tuple params | |
56 | 145 | |
146 | ## [v1.0.5] - 2015-02-03 | |
57 | 147 | ### Changed |
58 | - Documentation fixes | |
148 | - Add a default interval of 1 second for the constant generator | |
149 | - Improve on_predicate stop condition avoiding extra sleep | |
59 | 150 | |
60 | ## [v1.3.1] - 2016-08-08 | |
151 | ## [v1.0.4] - 2014-08-12 | |
152 | ### Added | |
153 | - Python 2.6 support from @Bonko | |
154 | - Python 3.0 support from @robyoung | |
155 | - Run tests in Travis from @robyoung | |
156 | ||
157 | ## [v1.0.3] - 2014-06-05 | |
61 | 158 | ### Changed |
62 | - Include README.rst in source distribution (fixes package) | |
63 | ||
64 | ## [v1.3.2] - 2016-11-18 | |
65 | ### Changed | |
66 | - Don't log retried args and kwargs by default | |
67 | - README.rst syntax highlighting from @dethi | |
68 | ||
69 | ## [v1.4.0] - 2017-02-05 | |
70 | ### Added | |
71 | - Async support via `asyncio` coroutines (Python 3.4) from @rutsky | |
72 | ||
73 | ### Changed | |
74 | - Refactor `backoff` module into package with identical API | |
75 | ||
76 | ## [v1.4.1] - 2017-04-21 | |
77 | ### Added | |
78 | - Expose __version__ at package root | |
79 | ||
80 | ### Changed | |
81 | - Fix checking for running sync version in coroutine in case when event | |
82 | loop is not set from @rutsky | |
83 | ||
84 | ## [v1.4.2] - 2017-04-25 | |
85 | ### Changed | |
86 | ||
87 | - Use documented logger name https://github.com/litl/backoff/pull/32 | |
88 | from @pquentin | |
89 | ||
90 | ## [v1.4.3] - 2017-05-22 | |
91 | ### Changed | |
92 | ||
93 | - Add license to source distribution | |
94 | ||
95 | ## [v1.5.0] - 2018-04-11 | |
96 | ### Changed | |
97 | ||
98 | - Add max_time keyword argument | |
99 | ||
100 | ## [v1.6.0] - 2018-07-14 | |
101 | ### Changed | |
102 | ||
103 | - Change default log level from ERROR to INFO | |
104 | - Log retries on exception as INFO | |
105 | ||
106 | ## [v1.7.0] - 2018-11-23 | |
107 | ### Changed | |
108 | ||
109 | - Support Python 3.7 | |
110 | - Drop support for async in Python 3.4 | |
111 | - Drop support for Python 2.6 | |
112 | - Update development dependencies | |
113 | - Use poetry for dependencies and packaging | |
114 | ||
115 | ## [v1.8.0] - 2018-12-20 | |
116 | ### Changed | |
117 | ||
118 | - Give up on StopIteration raised in wait generators | |
119 | - Iterable intervals for constant wait_gen for predefined wait sequences | |
120 | - Nullary jitter signature deprecation warning | |
121 | - Custom loggers | |
122 | ||
123 | ## [v1.8.1] - 2019-10-11 | |
124 | ### Changed | |
125 | ||
126 | - Use arguments in log messages rather than fully formatting log | |
127 | https://github.com/litl/backoff/pull/82 from @lbernick | |
128 | ||
129 | ## [v1.9.0] 2019-11-16 | |
130 | ### Changed | |
131 | ||
132 | - Support python 3.8 | |
133 | ||
134 | ## [v1.9.1] 2019-11-18 | |
135 | ### Changed | |
136 | ||
137 | - Include tests and changelog in distribution | |
138 | ||
139 | ## [v1.9.2] 2019-11-19 | |
140 | ### Changed | |
141 | ||
142 | - Don't include tests and changelog in distribution | |
143 | ||
144 | ## [v1.10.0] 2019-12-07 | |
145 | ### Changed | |
146 | ||
147 | - Allow sync decorator call from async function | |
148 | ||
149 | ## [v1.11.0] 2021-07-12 | |
150 | ### Changed | |
151 | ||
152 | - Configurable logging levels for backoff and giveup events | |
153 | - Minor documentation fixes | |
154 | ||
155 | ### NOTE | |
156 | ||
157 | THIS WILL BE THE FINAL PYTHON 2.7 COMPATIBLE RELEASE. | |
158 | ||
159 | ## [v1.11.1] 2021-07-14 | |
160 | ### Changed | |
161 | ||
162 | - Update __version__ in backoff module | |
159 | - Make logging unicode safe | |
160 | - Log on_predicate backoff as INFO rather than ERROR |
12 | 12 | @echo 'check make sure you are ready to commit' |
13 | 13 | |
14 | 14 | flake8: |
15 | ifeq ($(PY_GTE_35),1) | |
16 | 15 | @flake8 --ignore=E741,W503,W504 backoff tests |
17 | else | |
18 | @flake8 --ignore=E741,W503,W504 --exclude tests/python35,backoff/_async.py backoff tests | |
19 | endif | |
16 | ||
17 | mypy: | |
18 | @mypy --show-error-codes backoff tests | |
20 | 19 | |
21 | 20 | clean: |
22 | 21 | @find . -name "*.pyc" -delete |
24 | 23 | @rm -rf build dist .coverage MANIFEST |
25 | 24 | |
26 | 25 | test: clean |
27 | ifeq ($(PY_GTE_35),1) | |
28 | @PYTHONPATH=. py.test --cov-config .coveragerc-py35 --cov backoff tests | |
29 | else | |
30 | @PYTHONPATH=. py.test --cov-config .coveragerc-py2 --cov backoff tests/test_*.py | |
31 | endif | |
26 | @PYTHONPATH=. py.test --cov-report term-missing --cov backoff tests | |
32 | 27 | |
33 | check: flake8 test | |
34 | @coverage report | grep 100% >/dev/null || { echo 'Unit tests coverage is incomplete.'; exit 1; } | |
28 | check: flake8 mypy test | |
29 | @coverage report | grep ^TOTAL | grep 100% >/dev/null || \ | |
30 | { echo 'Unit tests coverage is incomplete.'; exit 1; } |
0 | 0 | backoff |
1 | 1 | ======= |
2 | 2 | |
3 | .. image:: https://travis-ci.org/litl/backoff.svg?branch=master | |
4 | :target: https://travis-ci.org/litl/backoff?branch=master | |
5 | .. image:: https://coveralls.io/repos/litl/backoff/badge.svg?branch=master | |
6 | :target: https://coveralls.io/r/litl/backoff?branch=master | |
3 | .. image:: https://travis-ci.org/litl/backoff.svg | |
4 | :target: https://travis-ci.org/litl/backoff | |
5 | .. image:: https://coveralls.io/repos/litl/backoff/badge.svg | |
6 | :target: https://coveralls.io/r/litl/backoff?branch=python-3 | |
7 | .. image:: https://github.com/litl/backoff/workflows/CodeQL/badge.svg | |
8 | :target: https://github.com/litl/backoff/actions/workflows/codeql-analysis.yml | |
7 | 9 | .. image:: https://img.shields.io/pypi/v/backoff.svg |
8 | 10 | :target: https://pypi.python.org/pypi/backoff |
11 | .. image:: https://img.shields.io/github/license/litl/backoff | |
12 | :target: https://github.com/litl/backoff/blob/master/LICENSE | |
9 | 13 | |
10 | 14 | **Function decoration for backoff and retry** |
11 | 15 | |
17 | 21 | polling resources for externally generated content. |
18 | 22 | |
19 | 23 | Decorators support both regular functions for synchronous code and |
20 | `asyncio <https://docs.python.org/3/library/asyncio.html>`_'s coroutines | |
24 | `asyncio <https://docs.python.org/3/library/asyncio.html>`__'s coroutines | |
21 | 25 | for asynchronous code. |
22 | 26 | |
23 | 27 | Examples |
101 | 105 | def get_url(url): |
102 | 106 | return requests.get(url) |
103 | 107 | |
104 | When a give up event occurs, the exception in question is reraised | |
108 | By default, when a give up event occurs, the exception in question is reraised | |
105 | 109 | and so code calling an `on_exception`-decorated function may still |
106 | need to do exception handling. | |
110 | need to do exception handling. This behavior can optionally be disabled | |
111 | using the `raise_on_giveup` keyword argument. | |
112 | ||
113 | In the code below, `requests.exceptions.RequestException` will not be raised | |
114 | when giveup occurs. Note that the decorated function will return `None` in this | |
115 | case, regardless of the logic in the `on_exception` handler. | |
116 | ||
117 | .. code-block:: python | |
118 | ||
119 | def fatal_code(e): | |
120 | return 400 <= e.response.status_code < 500 | |
121 | ||
122 | @backoff.on_exception(backoff.expo, | |
123 | requests.exceptions.RequestException, | |
124 | max_time=300, | |
125 | raise_on_giveup=False, | |
126 | giveup=fatal_code) | |
127 | def get_url(url): | |
128 | return requests.get(url) | |
129 | ||
130 | This is useful for non-mission critical code where you still wish to retry | |
131 | the code inside of `backoff.on_exception` but wish to proceed with execution | |
132 | even if all retries fail. | |
107 | 133 | |
108 | 134 | @backoff.on_predicate |
109 | 135 | --------------------- |
131 | 157 | .. code-block:: python |
132 | 158 | |
133 | 159 | @backoff.on_predicate(backoff.fibo, max_value=13) |
134 | def poll_for_message(queue) | |
160 | def poll_for_message(queue): | |
135 | 161 | return queue.get() |
136 | 162 | |
137 | 163 | More simply, a function which continues polling every second until it |
140 | 166 | .. code-block:: python |
141 | 167 | |
142 | 168 | @backoff.on_predicate(backoff.constant, interval=1) |
143 | def poll_for_message(queue) | |
169 | def poll_for_message(queue): | |
144 | 170 | return queue.get() |
145 | 171 | |
146 | 172 | Jitter |
179 | 205 | max_time=300) |
180 | 206 | def poll_for_message(queue): |
181 | 207 | return queue.get() |
208 | ||
182 | 209 | |
183 | 210 | Runtime Configuration |
184 | 211 | --------------------- |
266 | 293 | Backoff supports asynchronous execution in Python 3.5 and above. |
267 | 294 | |
268 | 295 | To use backoff in asynchronous code based on |
269 | `asyncio <https://docs.python.org/3/library/asyncio.html>`_ | |
296 | `asyncio <https://docs.python.org/3/library/asyncio.html>`__ | |
270 | 297 | you simply need to apply ``backoff.on_exception`` or ``backoff.on_predicate`` |
271 | 298 | to coroutines. |
272 | 299 | You can also use coroutines for the ``on_success``, ``on_backoff``, and |
273 | 300 | ``on_giveup`` event handlers, with the interface otherwise being identical. |
274 | 301 | |
275 | The following examples use `aiohttp <https://aiohttp.readthedocs.io/>`_ | |
302 | The following examples use `aiohttp <https://aiohttp.readthedocs.io/>`__ | |
276 | 303 | asynchronous HTTP client/server library. |
277 | 304 | |
278 | 305 | .. code-block:: python |
11 | 11 | For examples and full documentation see the README at |
12 | 12 | https://github.com/litl/backoff |
13 | 13 | """ |
14 | import sys | |
15 | import warnings | |
16 | ||
17 | 14 | from backoff._decorator import on_predicate, on_exception |
18 | 15 | from backoff._jitter import full_jitter, random_jitter |
19 | from backoff._wait_gen import constant, expo, fibo | |
16 | from backoff._wait_gen import constant, expo, fibo, runtime | |
20 | 17 | |
21 | 18 | __all__ = [ |
22 | 19 | 'on_predicate', |
24 | 21 | 'constant', |
25 | 22 | 'expo', |
26 | 23 | 'fibo', |
24 | 'runtime', | |
27 | 25 | 'full_jitter', |
28 | 26 | 'random_jitter', |
29 | 27 | ] |
30 | 28 | |
31 | __version__ = '1.11.1' | |
32 | ||
33 | ||
34 | if sys.version_info[0] < 3: | |
35 | warnings.warn( | |
36 | "Python 2.7 support is deprecated and will be dropped " | |
37 | "in the next release", | |
38 | DeprecationWarning, | |
39 | ) # pragma: no cover | |
29 | __version__ = '2.0.1' |
0 | 0 | # coding:utf-8 |
1 | 1 | import datetime |
2 | 2 | import functools |
3 | import asyncio # Python 3.5 code and syntax is allowed in this file | |
3 | import asyncio | |
4 | 4 | from datetime import timedelta |
5 | 5 | |
6 | 6 | from backoff._common import (_init_wait_gen, _maybe_call, _next_wait) |
20 | 20 | return [_ensure_coroutine(f) for f in coros_or_funcs] |
21 | 21 | |
22 | 22 | |
23 | async def _call_handlers(hdlrs, target, args, kwargs, tries, elapsed, **extra): | |
23 | async def _call_handlers(handlers, | |
24 | *, | |
25 | target, args, kwargs, tries, elapsed, | |
26 | **extra): | |
24 | 27 | details = { |
25 | 28 | 'target': target, |
26 | 29 | 'args': args, |
29 | 32 | 'elapsed': elapsed, |
30 | 33 | } |
31 | 34 | details.update(extra) |
32 | for hdlr in hdlrs: | |
33 | await hdlr(details) | |
35 | for handler in handlers: | |
36 | await handler(details) | |
34 | 37 | |
35 | 38 | |
36 | 39 | def retry_predicate(target, wait_gen, predicate, |
40 | *, | |
37 | 41 | max_tries, max_time, jitter, |
38 | 42 | on_success, on_backoff, on_giveup, |
39 | 43 | wait_gen_kwargs): |
50 | 54 | @functools.wraps(target) |
51 | 55 | async def retry(*args, **kwargs): |
52 | 56 | |
53 | # change names because python 2.x doesn't have nonlocal | |
54 | max_tries_ = _maybe_call(max_tries) | |
55 | max_time_ = _maybe_call(max_time) | |
57 | # update variables from outer function args | |
58 | nonlocal max_tries, max_time | |
59 | max_tries = _maybe_call(max_tries) | |
60 | max_time = _maybe_call(max_time) | |
56 | 61 | |
57 | 62 | tries = 0 |
58 | 63 | start = datetime.datetime.now() |
60 | 65 | while True: |
61 | 66 | tries += 1 |
62 | 67 | elapsed = timedelta.total_seconds(datetime.datetime.now() - start) |
63 | details = (target, args, kwargs, tries, elapsed) | |
68 | details = { | |
69 | "target": target, | |
70 | "args": args, | |
71 | "kwargs": kwargs, | |
72 | "tries": tries, | |
73 | "elapsed": elapsed, | |
74 | } | |
64 | 75 | |
65 | 76 | ret = await target(*args, **kwargs) |
66 | 77 | if predicate(ret): |
67 | max_tries_exceeded = (tries == max_tries_) | |
68 | max_time_exceeded = (max_time_ is not None and | |
69 | elapsed >= max_time_) | |
78 | max_tries_exceeded = (tries == max_tries) | |
79 | max_time_exceeded = (max_time is not None and | |
80 | elapsed >= max_time) | |
70 | 81 | |
71 | 82 | if max_tries_exceeded or max_time_exceeded: |
72 | await _call_handlers(on_giveup, *details, value=ret) | |
83 | await _call_handlers(on_giveup, **details, value=ret) | |
73 | 84 | break |
74 | 85 | |
75 | 86 | try: |
76 | seconds = _next_wait(wait, jitter, elapsed, max_time_) | |
87 | seconds = _next_wait(wait, ret, jitter, elapsed, max_time) | |
77 | 88 | except StopIteration: |
78 | await _call_handlers(on_giveup, *details, value=ret) | |
89 | await _call_handlers(on_giveup, **details, value=ret) | |
79 | 90 | break |
80 | 91 | |
81 | await _call_handlers(on_backoff, *details, value=ret, | |
92 | await _call_handlers(on_backoff, **details, value=ret, | |
82 | 93 | wait=seconds) |
83 | 94 | |
84 | 95 | # Note: there is no convenient way to pass explicit event |
93 | 104 | await asyncio.sleep(seconds) |
94 | 105 | continue |
95 | 106 | else: |
96 | await _call_handlers(on_success, *details, value=ret) | |
107 | await _call_handlers(on_success, **details, value=ret) | |
97 | 108 | break |
98 | 109 | |
99 | 110 | return ret |
102 | 113 | |
103 | 114 | |
104 | 115 | def retry_exception(target, wait_gen, exception, |
116 | *, | |
105 | 117 | max_tries, max_time, jitter, giveup, |
106 | on_success, on_backoff, on_giveup, | |
118 | on_success, on_backoff, on_giveup, raise_on_giveup, | |
107 | 119 | wait_gen_kwargs): |
108 | 120 | on_success = _ensure_coroutines(on_success) |
109 | 121 | on_backoff = _ensure_coroutines(on_backoff) |
116 | 128 | |
117 | 129 | @functools.wraps(target) |
118 | 130 | async def retry(*args, **kwargs): |
119 | # change names because python 2.x doesn't have nonlocal | |
120 | max_tries_ = _maybe_call(max_tries) | |
121 | max_time_ = _maybe_call(max_time) | |
131 | ||
132 | # update variables from outer function args | |
133 | nonlocal max_tries, max_time | |
134 | max_tries = _maybe_call(max_tries) | |
135 | max_time = _maybe_call(max_time) | |
122 | 136 | |
123 | 137 | tries = 0 |
124 | 138 | start = datetime.datetime.now() |
126 | 140 | while True: |
127 | 141 | tries += 1 |
128 | 142 | elapsed = timedelta.total_seconds(datetime.datetime.now() - start) |
129 | details = (target, args, kwargs, tries, elapsed) | |
143 | details = { | |
144 | "target": target, | |
145 | "args": args, | |
146 | "kwargs": kwargs, | |
147 | "tries": tries, | |
148 | "elapsed": elapsed, | |
149 | } | |
130 | 150 | |
131 | 151 | try: |
132 | 152 | ret = await target(*args, **kwargs) |
133 | 153 | except exception as e: |
134 | 154 | giveup_result = await giveup(e) |
135 | max_tries_exceeded = (tries == max_tries_) | |
136 | max_time_exceeded = (max_time_ is not None and | |
137 | elapsed >= max_time_) | |
155 | max_tries_exceeded = (tries == max_tries) | |
156 | max_time_exceeded = (max_time is not None and | |
157 | elapsed >= max_time) | |
138 | 158 | |
139 | 159 | if giveup_result or max_tries_exceeded or max_time_exceeded: |
140 | await _call_handlers(on_giveup, *details) | |
141 | raise | |
160 | await _call_handlers(on_giveup, **details) | |
161 | if raise_on_giveup: | |
162 | raise | |
163 | return None | |
142 | 164 | |
143 | 165 | try: |
144 | seconds = _next_wait(wait, jitter, elapsed, max_time_) | |
166 | seconds = _next_wait(wait, e, jitter, elapsed, max_time) | |
145 | 167 | except StopIteration: |
146 | await _call_handlers(on_giveup, *details) | |
168 | await _call_handlers(on_giveup, **details) | |
147 | 169 | raise e |
148 | 170 | |
149 | await _call_handlers(on_backoff, *details, wait=seconds) | |
171 | await _call_handlers(on_backoff, **details, wait=seconds) | |
150 | 172 | |
151 | 173 | # Note: there is no convenient way to pass explicit event |
152 | 174 | # loop to decorator, so here we assume that either default |
159 | 181 | # <https://bugs.python.org/issue28613> |
160 | 182 | await asyncio.sleep(seconds) |
161 | 183 | else: |
162 | await _call_handlers(on_success, *details) | |
184 | await _call_handlers(on_success, **details) | |
163 | 185 | |
164 | 186 | return ret |
165 | 187 | return retry |
4 | 4 | import sys |
5 | 5 | import traceback |
6 | 6 | import warnings |
7 | ||
8 | ||
9 | # python 2.7 -> 3.x compatibility for str and unicode | |
10 | try: | |
11 | basestring | |
12 | except NameError: # pragma: python=3.5 | |
13 | basestring = str | |
14 | 7 | |
15 | 8 | |
16 | 9 | # Use module-specific logger with a default null handler. |
21 | 14 | |
22 | 15 | # Evaluate arg that can be either a fixed value or a callable. |
23 | 16 | def _maybe_call(f, *args, **kwargs): |
24 | return f(*args, **kwargs) if callable(f) else f | |
17 | if callable(f): | |
18 | try: | |
19 | return f(*args, **kwargs) | |
20 | except TypeError: | |
21 | return f | |
22 | else: | |
23 | return f | |
25 | 24 | |
26 | 25 | |
27 | 26 | def _init_wait_gen(wait_gen, wait_gen_kwargs): |
28 | 27 | kwargs = {k: _maybe_call(v) for k, v in wait_gen_kwargs.items()} |
29 | return wait_gen(**kwargs) | |
28 | initialized = wait_gen(**kwargs) | |
29 | initialized.send(None) # Initialize with an empty send | |
30 | return initialized | |
30 | 31 | |
31 | 32 | |
32 | def _next_wait(wait, jitter, elapsed, max_time): | |
33 | value = next(wait) | |
33 | def _next_wait(wait, send_value, jitter, elapsed, max_time): | |
34 | value = wait.send(send_value) | |
34 | 35 | try: |
35 | 36 | if jitter is not None: |
36 | 37 | seconds = jitter(value) |
55 | 56 | |
56 | 57 | |
57 | 58 | def _prepare_logger(logger): |
58 | if isinstance(logger, basestring): | |
59 | if isinstance(logger, str): | |
59 | 60 | logger = logging.getLogger(logger) |
60 | 61 | return logger |
61 | 62 | |
63 | 64 | # Configure handler list with user specified handler and optionally |
64 | 65 | # with a default handler bound to the specified logger. |
65 | 66 | def _config_handlers( |
66 | user_handlers, default_handler=None, logger=None, log_level=None | |
67 | user_handlers, *, default_handler=None, logger=None, log_level=None | |
67 | 68 | ): |
68 | 69 | handlers = [] |
69 | 70 | if logger is not None: |
0 | 0 | # coding:utf-8 |
1 | from __future__ import unicode_literals | |
2 | ||
1 | import asyncio | |
3 | 2 | import logging |
4 | 3 | import operator |
5 | import sys | |
4 | from typing import Any, Callable, Iterable, Optional, Type, Union | |
6 | 5 | |
7 | 6 | from backoff._common import ( |
8 | 7 | _prepare_logger, |
11 | 10 | _log_giveup |
12 | 11 | ) |
13 | 12 | from backoff._jitter import full_jitter |
14 | from backoff import _sync | |
15 | ||
16 | ||
17 | def on_predicate(wait_gen, | |
18 | predicate=operator.not_, | |
19 | max_tries=None, | |
20 | max_time=None, | |
21 | jitter=full_jitter, | |
22 | on_success=None, | |
23 | on_backoff=None, | |
24 | on_giveup=None, | |
25 | logger='backoff', | |
26 | backoff_log_level=logging.INFO, | |
27 | giveup_log_level=logging.ERROR, | |
28 | **wait_gen_kwargs): | |
13 | from backoff import _async, _sync | |
14 | from backoff._typing import ( | |
15 | _CallableT, | |
16 | _Handler, | |
17 | _Jitterer, | |
18 | _MaybeCallable, | |
19 | _MaybeLogger, | |
20 | _MaybeSequence, | |
21 | _Predicate, | |
22 | _WaitGenerator, | |
23 | ) | |
24 | ||
25 | ||
26 | def on_predicate(wait_gen: _WaitGenerator, | |
27 | predicate: _Predicate[Any] = operator.not_, | |
28 | *, | |
29 | max_tries: Optional[_MaybeCallable[int]] = None, | |
30 | max_time: Optional[_MaybeCallable[float]] = None, | |
31 | jitter: Union[_Jitterer, None] = full_jitter, | |
32 | on_success: Union[_Handler, Iterable[_Handler]] = None, | |
33 | on_backoff: Union[_Handler, Iterable[_Handler]] = None, | |
34 | on_giveup: Union[_Handler, Iterable[_Handler]] = None, | |
35 | logger: _MaybeLogger = 'backoff', | |
36 | backoff_log_level: int = logging.INFO, | |
37 | giveup_log_level: int = logging.ERROR, | |
38 | **wait_gen_kwargs: Any) -> Callable[[_CallableT], _CallableT]: | |
29 | 39 | """Returns decorator for backoff and retry triggered by predicate. |
30 | 40 | |
31 | 41 | Args: |
70 | 80 | This is useful for runtime configuration. |
71 | 81 | """ |
72 | 82 | def decorate(target): |
73 | # change names because python 2.x doesn't have nonlocal | |
74 | logger_ = _prepare_logger(logger) | |
75 | ||
76 | on_success_ = _config_handlers(on_success) | |
77 | on_backoff_ = _config_handlers( | |
78 | on_backoff, _log_backoff, logger_, backoff_log_level | |
79 | ) | |
80 | on_giveup_ = _config_handlers( | |
81 | on_giveup, _log_giveup, logger_, giveup_log_level | |
82 | ) | |
83 | ||
84 | retry = None | |
85 | if sys.version_info >= (3, 5): # pragma: python=3.5 | |
86 | import asyncio | |
87 | ||
88 | if asyncio.iscoroutinefunction(target): | |
89 | import backoff._async | |
90 | retry = backoff._async.retry_predicate | |
91 | ||
92 | if retry is None: | |
83 | nonlocal logger, on_success, on_backoff, on_giveup | |
84 | ||
85 | logger = _prepare_logger(logger) | |
86 | on_success = _config_handlers(on_success) | |
87 | on_backoff = _config_handlers( | |
88 | on_backoff, | |
89 | default_handler=_log_backoff, | |
90 | logger=logger, | |
91 | log_level=backoff_log_level | |
92 | ) | |
93 | on_giveup = _config_handlers( | |
94 | on_giveup, | |
95 | default_handler=_log_giveup, | |
96 | logger=logger, | |
97 | log_level=giveup_log_level | |
98 | ) | |
99 | ||
100 | if asyncio.iscoroutinefunction(target): | |
101 | retry = _async.retry_predicate | |
102 | else: | |
93 | 103 | retry = _sync.retry_predicate |
94 | 104 | |
95 | return retry(target, wait_gen, predicate, | |
96 | max_tries, max_time, jitter, | |
97 | on_success_, on_backoff_, on_giveup_, | |
98 | wait_gen_kwargs) | |
105 | return retry( | |
106 | target, | |
107 | wait_gen, | |
108 | predicate, | |
109 | max_tries=max_tries, | |
110 | max_time=max_time, | |
111 | jitter=jitter, | |
112 | on_success=on_success, | |
113 | on_backoff=on_backoff, | |
114 | on_giveup=on_giveup, | |
115 | wait_gen_kwargs=wait_gen_kwargs | |
116 | ) | |
99 | 117 | |
100 | 118 | # Return a function which decorates a target with a retry loop. |
101 | 119 | return decorate |
102 | 120 | |
103 | 121 | |
104 | def on_exception(wait_gen, | |
105 | exception, | |
106 | max_tries=None, | |
107 | max_time=None, | |
108 | jitter=full_jitter, | |
109 | giveup=lambda e: False, | |
110 | on_success=None, | |
111 | on_backoff=None, | |
112 | on_giveup=None, | |
113 | logger='backoff', | |
114 | backoff_log_level=logging.INFO, | |
115 | giveup_log_level=logging.ERROR, | |
116 | **wait_gen_kwargs): | |
122 | def on_exception(wait_gen: _WaitGenerator, | |
123 | exception: _MaybeSequence[Type[Exception]], | |
124 | *, | |
125 | max_tries: Optional[_MaybeCallable[int]] = None, | |
126 | max_time: Optional[_MaybeCallable[float]] = None, | |
127 | jitter: Union[_Jitterer, None] = full_jitter, | |
128 | giveup: _Predicate[Exception] = lambda e: False, | |
129 | on_success: Union[_Handler, Iterable[_Handler]] = None, | |
130 | on_backoff: Union[_Handler, Iterable[_Handler]] = None, | |
131 | on_giveup: Union[_Handler, Iterable[_Handler]] = None, | |
132 | raise_on_giveup: bool = True, | |
133 | logger: _MaybeLogger = 'backoff', | |
134 | backoff_log_level: int = logging.INFO, | |
135 | giveup_log_level: int = logging.ERROR, | |
136 | **wait_gen_kwargs: Any) -> Callable[[_CallableT], _CallableT]: | |
117 | 137 | """Returns decorator for backoff and retry triggered by exception. |
118 | 138 | |
119 | 139 | Args: |
149 | 169 | signature to be called in the event that max_tries |
150 | 170 | is exceeded. The parameter is a dict containing details |
151 | 171 | about the invocation. |
172 | raise_on_giveup: Boolean indicating whether the registered exceptions | |
173 | should be raised on giveup. Defaults to `True` | |
152 | 174 | logger: Name or Logger object to log to. Defaults to 'backoff'. |
153 | 175 | backoff_log_level: log level for the backoff event. Defaults to "INFO" |
154 | 176 | giveup_log_level: log level for the give up event. Defaults to "ERROR" |
158 | 180 | This is useful for runtime configuration. |
159 | 181 | """ |
160 | 182 | def decorate(target): |
161 | # change names because python 2.x doesn't have nonlocal | |
162 | logger_ = _prepare_logger(logger) | |
163 | ||
164 | on_success_ = _config_handlers(on_success) | |
165 | on_backoff_ = _config_handlers( | |
166 | on_backoff, _log_backoff, logger_, backoff_log_level | |
167 | ) | |
168 | on_giveup_ = _config_handlers( | |
169 | on_giveup, _log_giveup, logger_, giveup_log_level | |
170 | ) | |
171 | ||
172 | retry = None | |
173 | if sys.version_info[:2] >= (3, 5): # pragma: python=3.5 | |
174 | import asyncio | |
175 | ||
176 | if asyncio.iscoroutinefunction(target): | |
177 | import backoff._async | |
178 | retry = backoff._async.retry_exception | |
179 | ||
180 | if retry is None: | |
183 | nonlocal logger, on_success, on_backoff, on_giveup | |
184 | ||
185 | logger = _prepare_logger(logger) | |
186 | on_success = _config_handlers(on_success) | |
187 | on_backoff = _config_handlers( | |
188 | on_backoff, | |
189 | default_handler=_log_backoff, | |
190 | logger=logger, | |
191 | log_level=backoff_log_level, | |
192 | ) | |
193 | on_giveup = _config_handlers( | |
194 | on_giveup, | |
195 | default_handler=_log_giveup, | |
196 | logger=logger, | |
197 | log_level=giveup_log_level, | |
198 | ) | |
199 | ||
200 | if asyncio.iscoroutinefunction(target): | |
201 | retry = _async.retry_exception | |
202 | else: | |
181 | 203 | retry = _sync.retry_exception |
182 | 204 | |
183 | return retry(target, wait_gen, exception, | |
184 | max_tries, max_time, jitter, giveup, | |
185 | on_success_, on_backoff_, on_giveup_, | |
186 | wait_gen_kwargs) | |
205 | return retry( | |
206 | target, | |
207 | wait_gen, | |
208 | exception, | |
209 | max_tries=max_tries, | |
210 | max_time=max_time, | |
211 | jitter=jitter, | |
212 | giveup=giveup, | |
213 | on_success=on_success, | |
214 | on_backoff=on_backoff, | |
215 | on_giveup=on_giveup, | |
216 | raise_on_giveup=raise_on_giveup, | |
217 | wait_gen_kwargs=wait_gen_kwargs | |
218 | ) | |
187 | 219 | |
188 | 220 | # Return a function which decorates a target with a retry loop. |
189 | 221 | return decorate |
2 | 2 | import random |
3 | 3 | |
4 | 4 | |
5 | def random_jitter(value): | |
5 | def random_jitter(value: float) -> float: | |
6 | 6 | """Jitter the value a random number of milliseconds. |
7 | 7 | |
8 | 8 | This adds up to 1 second of additional time to the original value. |
14 | 14 | return value + random.random() |
15 | 15 | |
16 | 16 | |
17 | def full_jitter(value): | |
17 | def full_jitter(value: float) -> float: | |
18 | 18 | """Jitter the value across the full range (0 to value). |
19 | 19 | |
20 | 20 | This corresponds to the "Full Jitter" algorithm specified in the |
20 | 20 | |
21 | 21 | |
22 | 22 | def retry_predicate(target, wait_gen, predicate, |
23 | *, | |
23 | 24 | max_tries, max_time, jitter, |
24 | 25 | on_success, on_backoff, on_giveup, |
25 | 26 | wait_gen_kwargs): |
27 | 28 | @functools.wraps(target) |
28 | 29 | def retry(*args, **kwargs): |
29 | 30 | |
30 | # change names because python 2.x doesn't have nonlocal | |
31 | max_tries_ = _maybe_call(max_tries) | |
32 | max_time_ = _maybe_call(max_time) | |
31 | # update variables from outer function args | |
32 | nonlocal max_tries, max_time | |
33 | max_tries = _maybe_call(max_tries) | |
34 | max_time = _maybe_call(max_time) | |
33 | 35 | |
34 | 36 | tries = 0 |
35 | 37 | start = datetime.datetime.now() |
37 | 39 | while True: |
38 | 40 | tries += 1 |
39 | 41 | elapsed = timedelta.total_seconds(datetime.datetime.now() - start) |
40 | details = (target, args, kwargs, tries, elapsed) | |
42 | details = { | |
43 | "target": target, | |
44 | "args": args, | |
45 | "kwargs": kwargs, | |
46 | "tries": tries, | |
47 | "elapsed": elapsed, | |
48 | } | |
41 | 49 | |
42 | 50 | ret = target(*args, **kwargs) |
43 | 51 | if predicate(ret): |
44 | max_tries_exceeded = (tries == max_tries_) | |
45 | max_time_exceeded = (max_time_ is not None and | |
46 | elapsed >= max_time_) | |
52 | max_tries_exceeded = (tries == max_tries) | |
53 | max_time_exceeded = (max_time is not None and | |
54 | elapsed >= max_time) | |
47 | 55 | |
48 | 56 | if max_tries_exceeded or max_time_exceeded: |
49 | _call_handlers(on_giveup, *details, value=ret) | |
57 | _call_handlers(on_giveup, **details, value=ret) | |
50 | 58 | break |
51 | 59 | |
52 | 60 | try: |
53 | seconds = _next_wait(wait, jitter, elapsed, max_time_) | |
61 | seconds = _next_wait(wait, ret, jitter, elapsed, max_time) | |
54 | 62 | except StopIteration: |
55 | _call_handlers(on_giveup, *details) | |
63 | _call_handlers(on_giveup, **details) | |
56 | 64 | break |
57 | 65 | |
58 | _call_handlers(on_backoff, *details, | |
66 | _call_handlers(on_backoff, **details, | |
59 | 67 | value=ret, wait=seconds) |
60 | 68 | |
61 | 69 | time.sleep(seconds) |
62 | 70 | continue |
63 | 71 | else: |
64 | _call_handlers(on_success, *details, value=ret) | |
72 | _call_handlers(on_success, **details, value=ret) | |
65 | 73 | break |
66 | 74 | |
67 | 75 | return ret |
70 | 78 | |
71 | 79 | |
72 | 80 | def retry_exception(target, wait_gen, exception, |
81 | *, | |
73 | 82 | max_tries, max_time, jitter, giveup, |
74 | on_success, on_backoff, on_giveup, | |
83 | on_success, on_backoff, on_giveup, raise_on_giveup, | |
75 | 84 | wait_gen_kwargs): |
76 | 85 | |
77 | 86 | @functools.wraps(target) |
78 | 87 | def retry(*args, **kwargs): |
79 | 88 | |
80 | # change names because python 2.x doesn't have nonlocal | |
81 | max_tries_ = _maybe_call(max_tries) | |
82 | max_time_ = _maybe_call(max_time) | |
89 | # update variables from outer function args | |
90 | nonlocal max_tries, max_time | |
91 | max_tries = _maybe_call(max_tries) | |
92 | max_time = _maybe_call(max_time) | |
83 | 93 | |
84 | 94 | tries = 0 |
85 | 95 | start = datetime.datetime.now() |
87 | 97 | while True: |
88 | 98 | tries += 1 |
89 | 99 | elapsed = timedelta.total_seconds(datetime.datetime.now() - start) |
90 | details = (target, args, kwargs, tries, elapsed) | |
100 | details = { | |
101 | "target": target, | |
102 | "args": args, | |
103 | "kwargs": kwargs, | |
104 | "tries": tries, | |
105 | "elapsed": elapsed, | |
106 | } | |
91 | 107 | |
92 | 108 | try: |
93 | 109 | ret = target(*args, **kwargs) |
94 | 110 | except exception as e: |
95 | max_tries_exceeded = (tries == max_tries_) | |
96 | max_time_exceeded = (max_time_ is not None and | |
97 | elapsed >= max_time_) | |
111 | max_tries_exceeded = (tries == max_tries) | |
112 | max_time_exceeded = (max_time is not None and | |
113 | elapsed >= max_time) | |
98 | 114 | |
99 | 115 | if giveup(e) or max_tries_exceeded or max_time_exceeded: |
100 | _call_handlers(on_giveup, *details) | |
101 | raise | |
116 | _call_handlers(on_giveup, **details) | |
117 | if raise_on_giveup: | |
118 | raise | |
119 | return None | |
102 | 120 | |
103 | 121 | try: |
104 | seconds = _next_wait(wait, jitter, elapsed, max_time_) | |
122 | seconds = _next_wait(wait, e, jitter, elapsed, max_time) | |
105 | 123 | except StopIteration: |
106 | _call_handlers(on_giveup, *details) | |
124 | _call_handlers(on_giveup, **details) | |
107 | 125 | raise e |
108 | 126 | |
109 | _call_handlers(on_backoff, *details, wait=seconds) | |
127 | _call_handlers(on_backoff, **details, wait=seconds) | |
110 | 128 | |
111 | 129 | time.sleep(seconds) |
112 | 130 | else: |
113 | _call_handlers(on_success, *details) | |
131 | _call_handlers(on_success, **details) | |
114 | 132 | |
115 | 133 | return ret |
116 | 134 | return retry |
0 | # coding:utf-8 | |
1 | import logging | |
2 | import sys | |
3 | from typing import (Any, Callable, Dict, Generator, Sequence, Tuple, Union, | |
4 | TypeVar) | |
5 | ||
6 | ||
7 | details_kwargs = {"total": False} | |
8 | ||
9 | if sys.version_info >= (3, 8): # pragma: no cover | |
10 | from typing import TypedDict | |
11 | else: # pragma: no cover | |
12 | # use typing_extensions if installed but don't require it | |
13 | try: | |
14 | from typing_extensions import TypedDict | |
15 | except ImportError: | |
16 | TypedDict = Dict[str, Any] | |
17 | del details_kwargs["total"] | |
18 | ||
19 | ||
20 | class _Details(TypedDict): | |
21 | target: Callable[..., Any] | |
22 | args: Tuple[Any, ...] | |
23 | kwargs: Dict[str, Any] | |
24 | tries: int | |
25 | elapsed: float | |
26 | ||
27 | ||
28 | class Details(_Details, **details_kwargs): | |
29 | wait: float # present in the on_backoff handler case for either decorator | |
30 | value: Any # present in the on_predicate decorator case | |
31 | ||
32 | ||
33 | T = TypeVar("T") | |
34 | ||
35 | _CallableT = TypeVar('_CallableT', bound=Callable[..., Any]) | |
36 | _Handler = Callable[[Details], None] | |
37 | _Jitterer = Callable[[float], float] | |
38 | _MaybeCallable = Union[T, Callable[[], T]] | |
39 | _MaybeLogger = Union[str, logging.Logger, None] | |
40 | _MaybeSequence = Union[T, Sequence[T]] | |
41 | _Predicate = Callable[[T], bool] | |
42 | _WaitGenerator = Callable[..., Generator[float, None, None]] |
0 | 0 | # coding:utf-8 |
1 | 1 | |
2 | 2 | import itertools |
3 | from typing import Any, Callable, Generator, Iterable, Optional, Union | |
3 | 4 | |
4 | 5 | |
5 | def expo(base=2, factor=1, max_value=None): | |
6 | def expo( | |
7 | base: int = 2, | |
8 | factor: int = 1, | |
9 | max_value: Optional[int] = None | |
10 | ) -> Generator[int, Any, None]: | |
11 | ||
6 | 12 | """Generator for exponential decay. |
7 | 13 | |
8 | 14 | Args: |
12 | 18 | true exponential sequence exceeds this, the value |
13 | 19 | of max_value will forever after be yielded. |
14 | 20 | """ |
21 | # Advance past initial .send() call | |
22 | yield # type: ignore[misc] | |
15 | 23 | n = 0 |
16 | 24 | while True: |
17 | 25 | a = factor * base ** n |
22 | 30 | yield max_value |
23 | 31 | |
24 | 32 | |
25 | def fibo(max_value=None): | |
33 | def fibo(max_value: Optional[int] = None) -> Generator[int, None, None]: | |
26 | 34 | """Generator for fibonaccial decay. |
27 | 35 | |
28 | 36 | Args: |
30 | 38 | true fibonacci sequence exceeds this, the value |
31 | 39 | of max_value will forever after be yielded. |
32 | 40 | """ |
41 | # Advance past initial .send() call | |
42 | yield # type: ignore[misc] | |
43 | ||
33 | 44 | a = 1 |
34 | 45 | b = 1 |
35 | 46 | while True: |
40 | 51 | yield max_value |
41 | 52 | |
42 | 53 | |
43 | def constant(interval=1): | |
54 | def constant( | |
55 | interval: Union[int, Iterable[int]] = 1 | |
56 | ) -> Generator[int, None, None]: | |
44 | 57 | """Generator for constant intervals. |
45 | 58 | |
46 | 59 | Args: |
47 | 60 | interval: A constant value to yield or an iterable of such values. |
48 | 61 | """ |
62 | # Advance past initial .send() call | |
63 | yield # type: ignore[misc] | |
64 | ||
49 | 65 | try: |
50 | itr = iter(interval) | |
66 | itr = iter(interval) # type: ignore | |
51 | 67 | except TypeError: |
52 | itr = itertools.repeat(interval) | |
68 | itr = itertools.repeat(interval) # type: ignore | |
53 | 69 | |
54 | 70 | for val in itr: |
55 | 71 | yield val |
72 | ||
73 | ||
74 | def runtime(*, value: Callable[[Any], int]) -> Generator[int, None, None]: | |
75 | """Generator that is based on parsing the return value or thrown | |
76 | exception of the decorated method | |
77 | ||
78 | Args: | |
79 | value: a callable which takes as input the decorated | |
80 | function's return value or thrown exception and | |
81 | determines how long to wait | |
82 | """ | |
83 | ret_or_exc = yield # type: ignore[misc] | |
84 | while True: | |
85 | ret_or_exc = yield value(ret_or_exc) |
0 | backoff (1.11.1-0kali1) UNRELEASED; urgency=low | |
0 | backoff (2.0.1-0kali1) UNRELEASED; urgency=low | |
1 | 1 | |
2 | 2 | * New upstream release. |
3 | * New upstream release. | |
3 | 4 | |
4 | -- Kali Janitor <[email protected]> Wed, 26 Jan 2022 06:52:06 -0000 | |
5 | -- Kali Janitor <[email protected]> Mon, 16 May 2022 04:06:15 -0000 | |
5 | 6 | |
6 | 7 | backoff (1.10.0-0kali1) kali-dev; urgency=medium |
7 | 8 |
0 | 0 | [tool.poetry] |
1 | 1 | name = "backoff" |
2 | version = "1.11.1" | |
2 | version = "2.0.1" | |
3 | 3 | description = "Function decoration for backoff and retry" |
4 | 4 | authors = ["Bob Green <[email protected]>"] |
5 | 5 | readme = "README.rst" |
13 | 13 | 'Natural Language :: English', |
14 | 14 | 'Operating System :: OS Independent', |
15 | 15 | 'Programming Language :: Python', |
16 | 'Programming Language :: Python :: 2', | |
17 | 'Programming Language :: Python :: 2.7', | |
18 | 16 | 'Programming Language :: Python :: 3', |
19 | 'Programming Language :: Python :: 3.5', | |
20 | 'Programming Language :: Python :: 3.6', | |
21 | 17 | 'Programming Language :: Python :: 3.7', |
22 | 18 | 'Programming Language :: Python :: 3.8', |
23 | 19 | 'Programming Language :: Python :: 3.9', |
24 | 'Topic :: Internet :: WWW/HTTP', | |
20 | 'Programming Language :: Python :: 3.10', | |
21 | 'Topic :: Internet :: WWW/HTTP', | |
25 | 22 | 'Topic :: Software Development :: Libraries :: Python Modules', |
26 | 23 | 'Topic :: Utilities'] |
27 | 24 | packages = [ |
29 | 26 | ] |
30 | 27 | |
31 | 28 | [tool.poetry.dependencies] |
32 | python = "^2.7 || ^3.5" | |
29 | python = "^3.7" | |
33 | 30 | |
34 | 31 | [tool.poetry.dev-dependencies] |
35 | flake8 = "^3.6" | |
36 | pytest = "^4.0" | |
37 | pytest-cov = "^2.6" | |
38 | pytest-asyncio = {version = "^0.10.0",python = "^3.5"} | |
39 | autopep8 = "^1.5.7" | |
32 | flake8 = "^4.0.1" | |
33 | mypy = "^0.942" | |
34 | pytest = "^7.1.2" | |
35 | pytest-asyncio = "^0.18.3" | |
36 | pytest-cov = "^3.0.0" | |
37 | requests = "^2.26.0" | |
38 | responses = "^0.20.0" | |
39 | types-requests = "^2.27.20" | |
40 | 40 | |
41 | 41 | [build-system] |
42 | requires = ["poetry-core"] | |
42 | requires = ["poetry-core>=1.0.0"] | |
43 | 43 | build-backend = "poetry.core.masonry.api" |
0 | # coding:utf-8 | |
1 | ||
2 | import asyncio # Python 3.5 code and syntax is allowed in this file | |
3 | import backoff | |
4 | import pytest | |
5 | import random | |
6 | ||
7 | from tests.common import _log_hdlrs, _save_target | |
8 | ||
9 | ||
10 | async def _await_none(x): | |
11 | return None | |
12 | ||
13 | ||
14 | @pytest.mark.asyncio | |
15 | async def test_on_predicate(monkeypatch): | |
16 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
17 | ||
18 | @backoff.on_predicate(backoff.expo) | |
19 | async def return_true(log, n): | |
20 | val = (len(log) == n - 1) | |
21 | log.append(val) | |
22 | return val | |
23 | ||
24 | log = [] | |
25 | ret = await return_true(log, 3) | |
26 | assert ret is True | |
27 | assert 3 == len(log) | |
28 | ||
29 | ||
30 | @pytest.mark.asyncio | |
31 | async def test_on_predicate_max_tries(monkeypatch): | |
32 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
33 | ||
34 | @backoff.on_predicate(backoff.expo, jitter=None, max_tries=3) | |
35 | async def return_true(log, n): | |
36 | val = (len(log) == n) | |
37 | log.append(val) | |
38 | return val | |
39 | ||
40 | log = [] | |
41 | ret = await return_true(log, 10) | |
42 | assert ret is False | |
43 | assert 3 == len(log) | |
44 | ||
45 | ||
46 | @pytest.mark.asyncio | |
47 | async def test_on_exception(monkeypatch): | |
48 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
49 | ||
50 | @backoff.on_exception(backoff.expo, KeyError) | |
51 | async def keyerror_then_true(log, n): | |
52 | if len(log) == n: | |
53 | return True | |
54 | e = KeyError() | |
55 | log.append(e) | |
56 | raise e | |
57 | ||
58 | log = [] | |
59 | assert (await keyerror_then_true(log, 3)) is True | |
60 | assert 3 == len(log) | |
61 | ||
62 | ||
63 | @pytest.mark.asyncio | |
64 | async def test_on_exception_tuple(monkeypatch): | |
65 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
66 | ||
67 | @backoff.on_exception(backoff.expo, (KeyError, ValueError)) | |
68 | async def keyerror_valueerror_then_true(log): | |
69 | if len(log) == 2: | |
70 | return True | |
71 | if len(log) == 0: | |
72 | e = KeyError() | |
73 | if len(log) == 1: | |
74 | e = ValueError() | |
75 | log.append(e) | |
76 | raise e | |
77 | ||
78 | log = [] | |
79 | assert (await keyerror_valueerror_then_true(log)) is True | |
80 | assert 2 == len(log) | |
81 | assert isinstance(log[0], KeyError) | |
82 | assert isinstance(log[1], ValueError) | |
83 | ||
84 | ||
85 | @pytest.mark.asyncio | |
86 | async def test_on_exception_max_tries(monkeypatch): | |
87 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
88 | ||
89 | @backoff.on_exception(backoff.expo, KeyError, jitter=None, max_tries=3) | |
90 | async def keyerror_then_true(log, n, foo=None): | |
91 | if len(log) == n: | |
92 | return True | |
93 | e = KeyError() | |
94 | log.append(e) | |
95 | raise e | |
96 | ||
97 | log = [] | |
98 | with pytest.raises(KeyError): | |
99 | await keyerror_then_true(log, 10, foo="bar") | |
100 | ||
101 | assert 3 == len(log) | |
102 | ||
103 | ||
104 | @pytest.mark.asyncio | |
105 | async def test_on_exception_constant_iterable(monkeypatch): | |
106 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
107 | ||
108 | backoffs = [] | |
109 | giveups = [] | |
110 | successes = [] | |
111 | ||
112 | @backoff.on_exception( | |
113 | backoff.constant, | |
114 | KeyError, | |
115 | interval=(1, 2, 3), | |
116 | on_backoff=backoffs.append, | |
117 | on_giveup=giveups.append, | |
118 | on_success=successes.append, | |
119 | ) | |
120 | async def endless_exceptions(): | |
121 | raise KeyError('foo') | |
122 | ||
123 | with pytest.raises(KeyError): | |
124 | await endless_exceptions() | |
125 | ||
126 | assert len(backoffs) == 3 | |
127 | assert len(giveups) == 1 | |
128 | assert len(successes) == 0 | |
129 | ||
130 | ||
131 | @pytest.mark.asyncio | |
132 | async def test_on_exception_success_random_jitter(monkeypatch): | |
133 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
134 | ||
135 | log, log_success, log_backoff, log_giveup = _log_hdlrs() | |
136 | ||
137 | @backoff.on_exception(backoff.expo, | |
138 | Exception, | |
139 | on_success=log_success, | |
140 | on_backoff=log_backoff, | |
141 | on_giveup=log_giveup, | |
142 | jitter=backoff.random_jitter, | |
143 | factor=0.5) | |
144 | @_save_target | |
145 | async def succeeder(*args, **kwargs): | |
146 | # succeed after we've backed off twice | |
147 | if len(log['backoff']) < 2: | |
148 | raise ValueError("catch me") | |
149 | ||
150 | await succeeder(1, 2, 3, foo=1, bar=2) | |
151 | ||
152 | # we try 3 times, backing off twice before succeeding | |
153 | assert len(log['success']) == 1 | |
154 | assert len(log['backoff']) == 2 | |
155 | assert len(log['giveup']) == 0 | |
156 | ||
157 | for i in range(2): | |
158 | details = log['backoff'][i] | |
159 | assert details['wait'] >= 0.5 * 2 ** i | |
160 | ||
161 | ||
162 | @pytest.mark.asyncio | |
163 | async def test_on_exception_success_full_jitter(monkeypatch): | |
164 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
165 | ||
166 | log, log_success, log_backoff, log_giveup = _log_hdlrs() | |
167 | ||
168 | @backoff.on_exception(backoff.expo, | |
169 | Exception, | |
170 | on_success=log_success, | |
171 | on_backoff=log_backoff, | |
172 | on_giveup=log_giveup, | |
173 | jitter=backoff.full_jitter, | |
174 | factor=0.5) | |
175 | @_save_target | |
176 | async def succeeder(*args, **kwargs): | |
177 | # succeed after we've backed off twice | |
178 | if len(log['backoff']) < 2: | |
179 | raise ValueError("catch me") | |
180 | ||
181 | await succeeder(1, 2, 3, foo=1, bar=2) | |
182 | ||
183 | # we try 3 times, backing off twice before succeeding | |
184 | assert len(log['success']) == 1 | |
185 | assert len(log['backoff']) == 2 | |
186 | assert len(log['giveup']) == 0 | |
187 | ||
188 | for i in range(2): | |
189 | details = log['backoff'][i] | |
190 | assert details['wait'] <= 0.5 * 2 ** i | |
191 | ||
192 | ||
193 | @pytest.mark.asyncio | |
194 | async def test_on_exception_success(): | |
195 | log, log_success, log_backoff, log_giveup = _log_hdlrs() | |
196 | ||
197 | @backoff.on_exception(backoff.constant, | |
198 | Exception, | |
199 | on_success=log_success, | |
200 | on_backoff=log_backoff, | |
201 | on_giveup=log_giveup, | |
202 | jitter=None, | |
203 | interval=0) | |
204 | @_save_target | |
205 | async def succeeder(*args, **kwargs): | |
206 | # succeed after we've backed off twice | |
207 | if len(log['backoff']) < 2: | |
208 | raise ValueError("catch me") | |
209 | ||
210 | await succeeder(1, 2, 3, foo=1, bar=2) | |
211 | ||
212 | # we try 3 times, backing off twice before succeeding | |
213 | assert len(log['success']) == 1 | |
214 | assert len(log['backoff']) == 2 | |
215 | assert len(log['giveup']) == 0 | |
216 | ||
217 | for i in range(2): | |
218 | details = log['backoff'][i] | |
219 | elapsed = details.pop('elapsed') | |
220 | assert isinstance(elapsed, float) | |
221 | assert details == {'args': (1, 2, 3), | |
222 | 'kwargs': {'foo': 1, 'bar': 2}, | |
223 | 'target': succeeder._target, | |
224 | 'tries': i + 1, | |
225 | 'wait': 0} | |
226 | ||
227 | details = log['success'][0] | |
228 | elapsed = details.pop('elapsed') | |
229 | assert isinstance(elapsed, float) | |
230 | assert details == {'args': (1, 2, 3), | |
231 | 'kwargs': {'foo': 1, 'bar': 2}, | |
232 | 'target': succeeder._target, | |
233 | 'tries': 3} | |
234 | ||
235 | ||
236 | @pytest.mark.asyncio | |
237 | async def test_on_exception_giveup(): | |
238 | log, log_success, log_backoff, log_giveup = _log_hdlrs() | |
239 | ||
240 | @backoff.on_exception(backoff.constant, | |
241 | ValueError, | |
242 | on_success=log_success, | |
243 | on_backoff=log_backoff, | |
244 | on_giveup=log_giveup, | |
245 | max_tries=3, | |
246 | jitter=None, | |
247 | interval=0) | |
248 | @_save_target | |
249 | async def exceptor(*args, **kwargs): | |
250 | raise ValueError("catch me") | |
251 | ||
252 | with pytest.raises(ValueError): | |
253 | await exceptor(1, 2, 3, foo=1, bar=2) | |
254 | ||
255 | # we try 3 times, backing off twice and giving up once | |
256 | assert len(log['success']) == 0 | |
257 | assert len(log['backoff']) == 2 | |
258 | assert len(log['giveup']) == 1 | |
259 | ||
260 | details = log['giveup'][0] | |
261 | elapsed = details.pop('elapsed') | |
262 | assert isinstance(elapsed, float) | |
263 | assert details == {'args': (1, 2, 3), | |
264 | 'kwargs': {'foo': 1, 'bar': 2}, | |
265 | 'target': exceptor._target, | |
266 | 'tries': 3} | |
267 | ||
268 | ||
269 | @pytest.mark.asyncio | |
270 | async def test_on_exception_giveup_predicate(monkeypatch): | |
271 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
272 | ||
273 | def on_baz(e): | |
274 | return str(e) == "baz" | |
275 | ||
276 | vals = ["baz", "bar", "foo"] | |
277 | ||
278 | @backoff.on_exception(backoff.constant, | |
279 | ValueError, | |
280 | giveup=on_baz) | |
281 | async def foo_bar_baz(): | |
282 | raise ValueError(vals.pop()) | |
283 | ||
284 | with pytest.raises(ValueError): | |
285 | await foo_bar_baz() | |
286 | ||
287 | assert not vals | |
288 | ||
289 | ||
290 | @pytest.mark.asyncio | |
291 | async def test_on_exception_giveup_coro(monkeypatch): | |
292 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
293 | ||
294 | async def on_baz(e): | |
295 | return str(e) == "baz" | |
296 | ||
297 | vals = ["baz", "bar", "foo"] | |
298 | ||
299 | @backoff.on_exception(backoff.constant, | |
300 | ValueError, | |
301 | giveup=on_baz) | |
302 | async def foo_bar_baz(): | |
303 | raise ValueError(vals.pop()) | |
304 | ||
305 | with pytest.raises(ValueError): | |
306 | await foo_bar_baz() | |
307 | ||
308 | assert not vals | |
309 | ||
310 | ||
311 | @pytest.mark.asyncio | |
312 | async def test_on_predicate_success(): | |
313 | log, log_success, log_backoff, log_giveup = _log_hdlrs() | |
314 | ||
315 | @backoff.on_predicate(backoff.constant, | |
316 | on_success=log_success, | |
317 | on_backoff=log_backoff, | |
318 | on_giveup=log_giveup, | |
319 | jitter=None, | |
320 | interval=0) | |
321 | @_save_target | |
322 | async def success(*args, **kwargs): | |
323 | # succeed after we've backed off twice | |
324 | return len(log['backoff']) == 2 | |
325 | ||
326 | await success(1, 2, 3, foo=1, bar=2) | |
327 | ||
328 | # we try 3 times, backing off twice before succeeding | |
329 | assert len(log['success']) == 1 | |
330 | assert len(log['backoff']) == 2 | |
331 | assert len(log['giveup']) == 0 | |
332 | ||
333 | for i in range(2): | |
334 | details = log['backoff'][i] | |
335 | elapsed = details.pop('elapsed') | |
336 | assert isinstance(elapsed, float) | |
337 | assert details == {'args': (1, 2, 3), | |
338 | 'kwargs': {'foo': 1, 'bar': 2}, | |
339 | 'target': success._target, | |
340 | 'tries': i + 1, | |
341 | 'value': False, | |
342 | 'wait': 0} | |
343 | ||
344 | details = log['success'][0] | |
345 | elapsed = details.pop('elapsed') | |
346 | assert isinstance(elapsed, float) | |
347 | assert details == {'args': (1, 2, 3), | |
348 | 'kwargs': {'foo': 1, 'bar': 2}, | |
349 | 'target': success._target, | |
350 | 'tries': 3, | |
351 | 'value': True} | |
352 | ||
353 | ||
354 | @pytest.mark.asyncio | |
355 | async def test_on_predicate_giveup(): | |
356 | log, log_success, log_backoff, log_giveup = _log_hdlrs() | |
357 | ||
358 | @backoff.on_predicate(backoff.constant, | |
359 | on_success=log_success, | |
360 | on_backoff=log_backoff, | |
361 | on_giveup=log_giveup, | |
362 | max_tries=3, | |
363 | jitter=None, | |
364 | interval=0) | |
365 | @_save_target | |
366 | async def emptiness(*args, **kwargs): | |
367 | pass | |
368 | ||
369 | await emptiness(1, 2, 3, foo=1, bar=2) | |
370 | ||
371 | # we try 3 times, backing off twice and giving up once | |
372 | assert len(log['success']) == 0 | |
373 | assert len(log['backoff']) == 2 | |
374 | assert len(log['giveup']) == 1 | |
375 | ||
376 | details = log['giveup'][0] | |
377 | elapsed = details.pop('elapsed') | |
378 | assert isinstance(elapsed, float) | |
379 | assert details == {'args': (1, 2, 3), | |
380 | 'kwargs': {'foo': 1, 'bar': 2}, | |
381 | 'target': emptiness._target, | |
382 | 'tries': 3, | |
383 | 'value': None} | |
384 | ||
385 | ||
386 | @pytest.mark.asyncio | |
387 | async def test_on_predicate_iterable_handlers(): | |
388 | hdlrs = [_log_hdlrs() for _ in range(3)] | |
389 | ||
390 | @backoff.on_predicate(backoff.constant, | |
391 | on_success=(h[1] for h in hdlrs), | |
392 | on_backoff=(h[2] for h in hdlrs), | |
393 | on_giveup=(h[3] for h in hdlrs), | |
394 | max_tries=3, | |
395 | jitter=None, | |
396 | interval=0) | |
397 | @_save_target | |
398 | async def emptiness(*args, **kwargs): | |
399 | pass | |
400 | ||
401 | await emptiness(1, 2, 3, foo=1, bar=2) | |
402 | ||
403 | for i in range(3): | |
404 | assert len(hdlrs[i][0]['success']) == 0 | |
405 | assert len(hdlrs[i][0]['backoff']) == 2 | |
406 | assert len(hdlrs[i][0]['giveup']) == 1 | |
407 | ||
408 | details = dict(hdlrs[i][0]['giveup'][0]) | |
409 | elapsed = details.pop('elapsed') | |
410 | assert isinstance(elapsed, float) | |
411 | assert details == {'args': (1, 2, 3), | |
412 | 'kwargs': {'foo': 1, 'bar': 2}, | |
413 | 'target': emptiness._target, | |
414 | 'tries': 3, | |
415 | 'value': None} | |
416 | ||
417 | ||
418 | @pytest.mark.asyncio | |
419 | async def test_on_predicate_constant_iterable(monkeypatch): | |
420 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
421 | ||
422 | waits = [1, 2, 3, 6, 9] | |
423 | backoffs = [] | |
424 | giveups = [] | |
425 | successes = [] | |
426 | ||
427 | @backoff.on_predicate( | |
428 | backoff.constant, | |
429 | interval=waits, | |
430 | on_backoff=backoffs.append, | |
431 | on_giveup=giveups.append, | |
432 | on_success=successes.append, | |
433 | jitter=None, | |
434 | ) | |
435 | async def falsey(): | |
436 | return False | |
437 | ||
438 | assert not await falsey() | |
439 | ||
440 | assert len(backoffs) == len(waits) | |
441 | for i, wait in enumerate(waits): | |
442 | assert backoffs[i]['wait'] == wait | |
443 | ||
444 | assert len(giveups) == 1 | |
445 | assert len(successes) == 0 | |
446 | ||
447 | ||
448 | # To maintain backward compatibility, | |
449 | # on_predicate should support 0-argument jitter function. | |
450 | @pytest.mark.asyncio | |
451 | async def test_on_exception_success_0_arg_jitter(monkeypatch): | |
452 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
453 | monkeypatch.setattr('random.random', lambda: 0) | |
454 | ||
455 | log, log_success, log_backoff, log_giveup = _log_hdlrs() | |
456 | ||
457 | @backoff.on_exception(backoff.constant, | |
458 | Exception, | |
459 | on_success=log_success, | |
460 | on_backoff=log_backoff, | |
461 | on_giveup=log_giveup, | |
462 | jitter=random.random, | |
463 | interval=0) | |
464 | @_save_target | |
465 | async def succeeder(*args, **kwargs): | |
466 | # succeed after we've backed off twice | |
467 | if len(log['backoff']) < 2: | |
468 | raise ValueError("catch me") | |
469 | ||
470 | with pytest.deprecated_call(): | |
471 | await succeeder(1, 2, 3, foo=1, bar=2) | |
472 | ||
473 | # we try 3 times, backing off twice before succeeding | |
474 | assert len(log['success']) == 1 | |
475 | assert len(log['backoff']) == 2 | |
476 | assert len(log['giveup']) == 0 | |
477 | ||
478 | for i in range(2): | |
479 | details = log['backoff'][i] | |
480 | elapsed = details.pop('elapsed') | |
481 | assert isinstance(elapsed, float) | |
482 | assert details == {'args': (1, 2, 3), | |
483 | 'kwargs': {'foo': 1, 'bar': 2}, | |
484 | 'target': succeeder._target, | |
485 | 'tries': i + 1, | |
486 | 'wait': 0} | |
487 | ||
488 | details = log['success'][0] | |
489 | elapsed = details.pop('elapsed') | |
490 | assert isinstance(elapsed, float) | |
491 | assert details == {'args': (1, 2, 3), | |
492 | 'kwargs': {'foo': 1, 'bar': 2}, | |
493 | 'target': succeeder._target, | |
494 | 'tries': 3} | |
495 | ||
496 | ||
497 | # To maintain backward compatibility, | |
498 | # on_predicate should support 0-argument jitter function. | |
499 | @pytest.mark.asyncio | |
500 | async def test_on_predicate_success_0_arg_jitter(monkeypatch): | |
501 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
502 | monkeypatch.setattr('random.random', lambda: 0) | |
503 | ||
504 | log, log_success, log_backoff, log_giveup = _log_hdlrs() | |
505 | ||
506 | @backoff.on_predicate(backoff.constant, | |
507 | on_success=log_success, | |
508 | on_backoff=log_backoff, | |
509 | on_giveup=log_giveup, | |
510 | jitter=random.random, | |
511 | interval=0) | |
512 | @_save_target | |
513 | async def success(*args, **kwargs): | |
514 | # succeed after we've backed off twice | |
515 | return len(log['backoff']) == 2 | |
516 | ||
517 | with pytest.deprecated_call(): | |
518 | await success(1, 2, 3, foo=1, bar=2) | |
519 | ||
520 | # we try 3 times, backing off twice before succeeding | |
521 | assert len(log['success']) == 1 | |
522 | assert len(log['backoff']) == 2 | |
523 | assert len(log['giveup']) == 0 | |
524 | ||
525 | for i in range(2): | |
526 | details = log['backoff'][i] | |
527 | elapsed = details.pop('elapsed') | |
528 | assert isinstance(elapsed, float) | |
529 | assert details == {'args': (1, 2, 3), | |
530 | 'kwargs': {'foo': 1, 'bar': 2}, | |
531 | 'target': success._target, | |
532 | 'tries': i + 1, | |
533 | 'value': False, | |
534 | 'wait': 0} | |
535 | ||
536 | details = log['success'][0] | |
537 | elapsed = details.pop('elapsed') | |
538 | assert isinstance(elapsed, float) | |
539 | assert details == {'args': (1, 2, 3), | |
540 | 'kwargs': {'foo': 1, 'bar': 2}, | |
541 | 'target': success._target, | |
542 | 'tries': 3, | |
543 | 'value': True} | |
544 | ||
545 | ||
546 | @pytest.mark.asyncio | |
547 | async def test_on_exception_callable_max_tries(monkeypatch): | |
548 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
549 | ||
550 | def lookup_max_tries(): | |
551 | return 3 | |
552 | ||
553 | log = [] | |
554 | ||
555 | @backoff.on_exception(backoff.constant, | |
556 | ValueError, | |
557 | max_tries=lookup_max_tries) | |
558 | async def exceptor(): | |
559 | log.append(True) | |
560 | raise ValueError() | |
561 | ||
562 | with pytest.raises(ValueError): | |
563 | await exceptor() | |
564 | ||
565 | assert len(log) == 3 | |
566 | ||
567 | ||
568 | @pytest.mark.asyncio | |
569 | async def test_on_exception_callable_gen_kwargs(): | |
570 | ||
571 | def lookup_foo(): | |
572 | return "foo" | |
573 | ||
574 | def wait_gen(foo=None, bar=None): | |
575 | assert foo == "foo" | |
576 | assert bar == "bar" | |
577 | ||
578 | while True: | |
579 | yield 0 | |
580 | ||
581 | @backoff.on_exception(wait_gen, | |
582 | ValueError, | |
583 | max_tries=2, | |
584 | foo=lookup_foo, | |
585 | bar="bar") | |
586 | async def exceptor(): | |
587 | raise ValueError("aah") | |
588 | ||
589 | with pytest.raises(ValueError): | |
590 | await exceptor() | |
591 | ||
592 | ||
593 | @pytest.mark.asyncio | |
594 | async def test_on_exception_coro_cancelling(event_loop): | |
595 | sleep_started_event = asyncio.Event() | |
596 | ||
597 | @backoff.on_predicate(backoff.expo) | |
598 | async def coro(): | |
599 | sleep_started_event.set() | |
600 | ||
601 | try: | |
602 | await asyncio.sleep(10) | |
603 | except asyncio.CancelledError: | |
604 | return True | |
605 | ||
606 | return False | |
607 | ||
608 | task = event_loop.create_task(coro()) | |
609 | ||
610 | await sleep_started_event.wait() | |
611 | ||
612 | task.cancel() | |
613 | ||
614 | assert (await task) | |
615 | ||
616 | ||
617 | def test_on_predicate_on_regular_function_without_event_loop(monkeypatch): | |
618 | monkeypatch.setattr('time.sleep', lambda x: None) | |
619 | ||
620 | # Set default event loop to None. | |
621 | loop = asyncio.get_event_loop() | |
622 | asyncio.set_event_loop(None) | |
623 | ||
624 | try: | |
625 | @backoff.on_predicate(backoff.expo) | |
626 | def return_true(log, n): | |
627 | val = (len(log) == n - 1) | |
628 | log.append(val) | |
629 | return val | |
630 | ||
631 | log = [] | |
632 | ret = return_true(log, 3) | |
633 | assert ret is True | |
634 | assert 3 == len(log) | |
635 | ||
636 | finally: | |
637 | # Restore event loop. | |
638 | asyncio.set_event_loop(loop) | |
639 | ||
640 | ||
641 | def test_on_exception_on_regular_function_without_event_loop(monkeypatch): | |
642 | monkeypatch.setattr('time.sleep', lambda x: None) | |
643 | ||
644 | # Set default event loop to None. | |
645 | loop = asyncio.get_event_loop() | |
646 | asyncio.set_event_loop(None) | |
647 | ||
648 | try: | |
649 | @backoff.on_exception(backoff.expo, KeyError) | |
650 | def keyerror_then_true(log, n): | |
651 | if len(log) == n: | |
652 | return True | |
653 | e = KeyError() | |
654 | log.append(e) | |
655 | raise e | |
656 | ||
657 | log = [] | |
658 | assert keyerror_then_true(log, 3) is True | |
659 | assert 3 == len(log) | |
660 | ||
661 | finally: | |
662 | # Restore event loop. | |
663 | asyncio.set_event_loop(loop) |
259 | 259 | 'tries': 3} |
260 | 260 | |
261 | 261 | |
262 | def test_on_exception_giveup(): | |
262 | @pytest.mark.parametrize('raise_on_giveup', [True, False]) | |
263 | def test_on_exception_giveup(raise_on_giveup): | |
263 | 264 | backoffs, giveups, successes = [], [], [] |
264 | 265 | |
265 | 266 | @backoff.on_exception(backoff.constant, |
269 | 270 | on_giveup=giveups.append, |
270 | 271 | max_tries=3, |
271 | 272 | jitter=None, |
273 | raise_on_giveup=raise_on_giveup, | |
272 | 274 | interval=0) |
273 | 275 | @_save_target |
274 | 276 | def exceptor(*args, **kwargs): |
275 | 277 | raise ValueError("catch me") |
276 | 278 | |
277 | with pytest.raises(ValueError): | |
279 | if raise_on_giveup: | |
280 | with pytest.raises(ValueError): | |
281 | exceptor(1, 2, 3, foo=1, bar=2) | |
282 | else: | |
278 | 283 | exceptor(1, 2, 3, foo=1, bar=2) |
279 | 284 | |
280 | 285 | # we try 3 times, backing off twice and giving up once |
0 | # coding:utf-8 | |
1 | ||
2 | import asyncio # Python 3.5 code and syntax is allowed in this file | |
3 | import backoff | |
4 | import pytest | |
5 | import random | |
6 | ||
7 | from tests.common import _log_hdlrs, _save_target | |
8 | ||
9 | ||
10 | async def _await_none(x): | |
11 | return None | |
12 | ||
13 | ||
14 | @pytest.mark.asyncio | |
15 | async def test_on_predicate(monkeypatch): | |
16 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
17 | ||
18 | @backoff.on_predicate(backoff.expo) | |
19 | async def return_true(log, n): | |
20 | val = (len(log) == n - 1) | |
21 | log.append(val) | |
22 | return val | |
23 | ||
24 | log = [] | |
25 | ret = await return_true(log, 3) | |
26 | assert ret is True | |
27 | assert 3 == len(log) | |
28 | ||
29 | ||
30 | @pytest.mark.asyncio | |
31 | async def test_on_predicate_max_tries(monkeypatch): | |
32 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
33 | ||
34 | @backoff.on_predicate(backoff.expo, jitter=None, max_tries=3) | |
35 | async def return_true(log, n): | |
36 | val = (len(log) == n) | |
37 | log.append(val) | |
38 | return val | |
39 | ||
40 | log = [] | |
41 | ret = await return_true(log, 10) | |
42 | assert ret is False | |
43 | assert 3 == len(log) | |
44 | ||
45 | ||
46 | @pytest.mark.asyncio | |
47 | async def test_on_exception(monkeypatch): | |
48 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
49 | ||
50 | @backoff.on_exception(backoff.expo, KeyError) | |
51 | async def keyerror_then_true(log, n): | |
52 | if len(log) == n: | |
53 | return True | |
54 | e = KeyError() | |
55 | log.append(e) | |
56 | raise e | |
57 | ||
58 | log = [] | |
59 | assert (await keyerror_then_true(log, 3)) is True | |
60 | assert 3 == len(log) | |
61 | ||
62 | ||
63 | @pytest.mark.asyncio | |
64 | async def test_on_exception_tuple(monkeypatch): | |
65 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
66 | ||
67 | @backoff.on_exception(backoff.expo, (KeyError, ValueError)) | |
68 | async def keyerror_valueerror_then_true(log): | |
69 | if len(log) == 2: | |
70 | return True | |
71 | if len(log) == 0: | |
72 | e = KeyError() | |
73 | if len(log) == 1: | |
74 | e = ValueError() | |
75 | log.append(e) | |
76 | raise e | |
77 | ||
78 | log = [] | |
79 | assert (await keyerror_valueerror_then_true(log)) is True | |
80 | assert 2 == len(log) | |
81 | assert isinstance(log[0], KeyError) | |
82 | assert isinstance(log[1], ValueError) | |
83 | ||
84 | ||
85 | @pytest.mark.asyncio | |
86 | async def test_on_exception_max_tries(monkeypatch): | |
87 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
88 | ||
89 | @backoff.on_exception(backoff.expo, KeyError, jitter=None, max_tries=3) | |
90 | async def keyerror_then_true(log, n, foo=None): | |
91 | if len(log) == n: | |
92 | return True | |
93 | e = KeyError() | |
94 | log.append(e) | |
95 | raise e | |
96 | ||
97 | log = [] | |
98 | with pytest.raises(KeyError): | |
99 | await keyerror_then_true(log, 10, foo="bar") | |
100 | ||
101 | assert 3 == len(log) | |
102 | ||
103 | ||
104 | @pytest.mark.asyncio | |
105 | async def test_on_exception_constant_iterable(monkeypatch): | |
106 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
107 | ||
108 | backoffs = [] | |
109 | giveups = [] | |
110 | successes = [] | |
111 | ||
112 | @backoff.on_exception( | |
113 | backoff.constant, | |
114 | KeyError, | |
115 | interval=(1, 2, 3), | |
116 | on_backoff=backoffs.append, | |
117 | on_giveup=giveups.append, | |
118 | on_success=successes.append, | |
119 | ) | |
120 | async def endless_exceptions(): | |
121 | raise KeyError('foo') | |
122 | ||
123 | with pytest.raises(KeyError): | |
124 | await endless_exceptions() | |
125 | ||
126 | assert len(backoffs) == 3 | |
127 | assert len(giveups) == 1 | |
128 | assert len(successes) == 0 | |
129 | ||
130 | ||
131 | @pytest.mark.asyncio | |
132 | async def test_on_exception_success_random_jitter(monkeypatch): | |
133 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
134 | ||
135 | log, log_success, log_backoff, log_giveup = _log_hdlrs() | |
136 | ||
137 | @backoff.on_exception(backoff.expo, | |
138 | Exception, | |
139 | on_success=log_success, | |
140 | on_backoff=log_backoff, | |
141 | on_giveup=log_giveup, | |
142 | jitter=backoff.random_jitter, | |
143 | factor=0.5) | |
144 | @_save_target | |
145 | async def succeeder(*args, **kwargs): | |
146 | # succeed after we've backed off twice | |
147 | if len(log['backoff']) < 2: | |
148 | raise ValueError("catch me") | |
149 | ||
150 | await succeeder(1, 2, 3, foo=1, bar=2) | |
151 | ||
152 | # we try 3 times, backing off twice before succeeding | |
153 | assert len(log['success']) == 1 | |
154 | assert len(log['backoff']) == 2 | |
155 | assert len(log['giveup']) == 0 | |
156 | ||
157 | for i in range(2): | |
158 | details = log['backoff'][i] | |
159 | assert details['wait'] >= 0.5 * 2 ** i | |
160 | ||
161 | ||
162 | @pytest.mark.asyncio | |
163 | async def test_on_exception_success_full_jitter(monkeypatch): | |
164 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
165 | ||
166 | log, log_success, log_backoff, log_giveup = _log_hdlrs() | |
167 | ||
168 | @backoff.on_exception(backoff.expo, | |
169 | Exception, | |
170 | on_success=log_success, | |
171 | on_backoff=log_backoff, | |
172 | on_giveup=log_giveup, | |
173 | jitter=backoff.full_jitter, | |
174 | factor=0.5) | |
175 | @_save_target | |
176 | async def succeeder(*args, **kwargs): | |
177 | # succeed after we've backed off twice | |
178 | if len(log['backoff']) < 2: | |
179 | raise ValueError("catch me") | |
180 | ||
181 | await succeeder(1, 2, 3, foo=1, bar=2) | |
182 | ||
183 | # we try 3 times, backing off twice before succeeding | |
184 | assert len(log['success']) == 1 | |
185 | assert len(log['backoff']) == 2 | |
186 | assert len(log['giveup']) == 0 | |
187 | ||
188 | for i in range(2): | |
189 | details = log['backoff'][i] | |
190 | assert details['wait'] <= 0.5 * 2 ** i | |
191 | ||
192 | ||
193 | @pytest.mark.asyncio | |
194 | async def test_on_exception_success(): | |
195 | log, log_success, log_backoff, log_giveup = _log_hdlrs() | |
196 | ||
197 | @backoff.on_exception(backoff.constant, | |
198 | Exception, | |
199 | on_success=log_success, | |
200 | on_backoff=log_backoff, | |
201 | on_giveup=log_giveup, | |
202 | jitter=None, | |
203 | interval=0) | |
204 | @_save_target | |
205 | async def succeeder(*args, **kwargs): | |
206 | # succeed after we've backed off twice | |
207 | if len(log['backoff']) < 2: | |
208 | raise ValueError("catch me") | |
209 | ||
210 | await succeeder(1, 2, 3, foo=1, bar=2) | |
211 | ||
212 | # we try 3 times, backing off twice before succeeding | |
213 | assert len(log['success']) == 1 | |
214 | assert len(log['backoff']) == 2 | |
215 | assert len(log['giveup']) == 0 | |
216 | ||
217 | for i in range(2): | |
218 | details = log['backoff'][i] | |
219 | elapsed = details.pop('elapsed') | |
220 | assert isinstance(elapsed, float) | |
221 | assert details == {'args': (1, 2, 3), | |
222 | 'kwargs': {'foo': 1, 'bar': 2}, | |
223 | 'target': succeeder._target, | |
224 | 'tries': i + 1, | |
225 | 'wait': 0} | |
226 | ||
227 | details = log['success'][0] | |
228 | elapsed = details.pop('elapsed') | |
229 | assert isinstance(elapsed, float) | |
230 | assert details == {'args': (1, 2, 3), | |
231 | 'kwargs': {'foo': 1, 'bar': 2}, | |
232 | 'target': succeeder._target, | |
233 | 'tries': 3} | |
234 | ||
235 | ||
236 | @pytest.mark.asyncio | |
237 | @pytest.mark.parametrize('raise_on_giveup', [True, False]) | |
238 | async def test_on_exception_giveup(raise_on_giveup): | |
239 | log, log_success, log_backoff, log_giveup = _log_hdlrs() | |
240 | ||
241 | @backoff.on_exception(backoff.constant, | |
242 | ValueError, | |
243 | on_success=log_success, | |
244 | on_backoff=log_backoff, | |
245 | on_giveup=log_giveup, | |
246 | raise_on_giveup=raise_on_giveup, | |
247 | max_tries=3, | |
248 | jitter=None, | |
249 | interval=0) | |
250 | @_save_target | |
251 | async def exceptor(*args, **kwargs): | |
252 | raise ValueError("catch me") | |
253 | ||
254 | if raise_on_giveup: | |
255 | with pytest.raises(ValueError): | |
256 | await exceptor(1, 2, 3, foo=1, bar=2) | |
257 | else: | |
258 | await exceptor(1, 2, 3, foo=1, bar=2) | |
259 | ||
260 | # we try 3 times, backing off twice and giving up once | |
261 | assert len(log['success']) == 0 | |
262 | assert len(log['backoff']) == 2 | |
263 | assert len(log['giveup']) == 1 | |
264 | ||
265 | details = log['giveup'][0] | |
266 | elapsed = details.pop('elapsed') | |
267 | assert isinstance(elapsed, float) | |
268 | assert details == {'args': (1, 2, 3), | |
269 | 'kwargs': {'foo': 1, 'bar': 2}, | |
270 | 'target': exceptor._target, | |
271 | 'tries': 3} | |
272 | ||
273 | ||
274 | @pytest.mark.asyncio | |
275 | async def test_on_exception_giveup_predicate(monkeypatch): | |
276 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
277 | ||
278 | def on_baz(e): | |
279 | return str(e) == "baz" | |
280 | ||
281 | vals = ["baz", "bar", "foo"] | |
282 | ||
283 | @backoff.on_exception(backoff.constant, | |
284 | ValueError, | |
285 | giveup=on_baz) | |
286 | async def foo_bar_baz(): | |
287 | raise ValueError(vals.pop()) | |
288 | ||
289 | with pytest.raises(ValueError): | |
290 | await foo_bar_baz() | |
291 | ||
292 | assert not vals | |
293 | ||
294 | ||
295 | @pytest.mark.asyncio | |
296 | async def test_on_exception_giveup_coro(monkeypatch): | |
297 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
298 | ||
299 | async def on_baz(e): | |
300 | return str(e) == "baz" | |
301 | ||
302 | vals = ["baz", "bar", "foo"] | |
303 | ||
304 | @backoff.on_exception(backoff.constant, | |
305 | ValueError, | |
306 | giveup=on_baz) | |
307 | async def foo_bar_baz(): | |
308 | raise ValueError(vals.pop()) | |
309 | ||
310 | with pytest.raises(ValueError): | |
311 | await foo_bar_baz() | |
312 | ||
313 | assert not vals | |
314 | ||
315 | ||
316 | @pytest.mark.asyncio | |
317 | async def test_on_predicate_success(): | |
318 | log, log_success, log_backoff, log_giveup = _log_hdlrs() | |
319 | ||
320 | @backoff.on_predicate(backoff.constant, | |
321 | on_success=log_success, | |
322 | on_backoff=log_backoff, | |
323 | on_giveup=log_giveup, | |
324 | jitter=None, | |
325 | interval=0) | |
326 | @_save_target | |
327 | async def success(*args, **kwargs): | |
328 | # succeed after we've backed off twice | |
329 | return len(log['backoff']) == 2 | |
330 | ||
331 | await success(1, 2, 3, foo=1, bar=2) | |
332 | ||
333 | # we try 3 times, backing off twice before succeeding | |
334 | assert len(log['success']) == 1 | |
335 | assert len(log['backoff']) == 2 | |
336 | assert len(log['giveup']) == 0 | |
337 | ||
338 | for i in range(2): | |
339 | details = log['backoff'][i] | |
340 | elapsed = details.pop('elapsed') | |
341 | assert isinstance(elapsed, float) | |
342 | assert details == {'args': (1, 2, 3), | |
343 | 'kwargs': {'foo': 1, 'bar': 2}, | |
344 | 'target': success._target, | |
345 | 'tries': i + 1, | |
346 | 'value': False, | |
347 | 'wait': 0} | |
348 | ||
349 | details = log['success'][0] | |
350 | elapsed = details.pop('elapsed') | |
351 | assert isinstance(elapsed, float) | |
352 | assert details == {'args': (1, 2, 3), | |
353 | 'kwargs': {'foo': 1, 'bar': 2}, | |
354 | 'target': success._target, | |
355 | 'tries': 3, | |
356 | 'value': True} | |
357 | ||
358 | ||
359 | @pytest.mark.asyncio | |
360 | async def test_on_predicate_giveup(): | |
361 | log, log_success, log_backoff, log_giveup = _log_hdlrs() | |
362 | ||
363 | @backoff.on_predicate(backoff.constant, | |
364 | on_success=log_success, | |
365 | on_backoff=log_backoff, | |
366 | on_giveup=log_giveup, | |
367 | max_tries=3, | |
368 | jitter=None, | |
369 | interval=0) | |
370 | @_save_target | |
371 | async def emptiness(*args, **kwargs): | |
372 | pass | |
373 | ||
374 | await emptiness(1, 2, 3, foo=1, bar=2) | |
375 | ||
376 | # we try 3 times, backing off twice and giving up once | |
377 | assert len(log['success']) == 0 | |
378 | assert len(log['backoff']) == 2 | |
379 | assert len(log['giveup']) == 1 | |
380 | ||
381 | details = log['giveup'][0] | |
382 | elapsed = details.pop('elapsed') | |
383 | assert isinstance(elapsed, float) | |
384 | assert details == {'args': (1, 2, 3), | |
385 | 'kwargs': {'foo': 1, 'bar': 2}, | |
386 | 'target': emptiness._target, | |
387 | 'tries': 3, | |
388 | 'value': None} | |
389 | ||
390 | ||
391 | @pytest.mark.asyncio | |
392 | async def test_on_predicate_iterable_handlers(): | |
393 | hdlrs = [_log_hdlrs() for _ in range(3)] | |
394 | ||
395 | @backoff.on_predicate(backoff.constant, | |
396 | on_success=(h[1] for h in hdlrs), | |
397 | on_backoff=(h[2] for h in hdlrs), | |
398 | on_giveup=(h[3] for h in hdlrs), | |
399 | max_tries=3, | |
400 | jitter=None, | |
401 | interval=0) | |
402 | @_save_target | |
403 | async def emptiness(*args, **kwargs): | |
404 | pass | |
405 | ||
406 | await emptiness(1, 2, 3, foo=1, bar=2) | |
407 | ||
408 | for i in range(3): | |
409 | assert len(hdlrs[i][0]['success']) == 0 | |
410 | assert len(hdlrs[i][0]['backoff']) == 2 | |
411 | assert len(hdlrs[i][0]['giveup']) == 1 | |
412 | ||
413 | details = dict(hdlrs[i][0]['giveup'][0]) | |
414 | elapsed = details.pop('elapsed') | |
415 | assert isinstance(elapsed, float) | |
416 | assert details == {'args': (1, 2, 3), | |
417 | 'kwargs': {'foo': 1, 'bar': 2}, | |
418 | 'target': emptiness._target, | |
419 | 'tries': 3, | |
420 | 'value': None} | |
421 | ||
422 | ||
423 | @pytest.mark.asyncio | |
424 | async def test_on_predicate_constant_iterable(monkeypatch): | |
425 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
426 | ||
427 | waits = [1, 2, 3, 6, 9] | |
428 | backoffs = [] | |
429 | giveups = [] | |
430 | successes = [] | |
431 | ||
432 | @backoff.on_predicate( | |
433 | backoff.constant, | |
434 | interval=waits, | |
435 | on_backoff=backoffs.append, | |
436 | on_giveup=giveups.append, | |
437 | on_success=successes.append, | |
438 | jitter=None, | |
439 | ) | |
440 | async def falsey(): | |
441 | return False | |
442 | ||
443 | assert not await falsey() | |
444 | ||
445 | assert len(backoffs) == len(waits) | |
446 | for i, wait in enumerate(waits): | |
447 | assert backoffs[i]['wait'] == wait | |
448 | ||
449 | assert len(giveups) == 1 | |
450 | assert len(successes) == 0 | |
451 | ||
452 | ||
453 | # To maintain backward compatibility, | |
454 | # on_predicate should support 0-argument jitter function. | |
455 | @pytest.mark.asyncio | |
456 | async def test_on_exception_success_0_arg_jitter(monkeypatch): | |
457 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
458 | monkeypatch.setattr('random.random', lambda: 0) | |
459 | ||
460 | log, log_success, log_backoff, log_giveup = _log_hdlrs() | |
461 | ||
462 | @backoff.on_exception(backoff.constant, | |
463 | Exception, | |
464 | on_success=log_success, | |
465 | on_backoff=log_backoff, | |
466 | on_giveup=log_giveup, | |
467 | jitter=random.random, | |
468 | interval=0) | |
469 | @_save_target | |
470 | async def succeeder(*args, **kwargs): | |
471 | # succeed after we've backed off twice | |
472 | if len(log['backoff']) < 2: | |
473 | raise ValueError("catch me") | |
474 | ||
475 | with pytest.deprecated_call(): | |
476 | await succeeder(1, 2, 3, foo=1, bar=2) | |
477 | ||
478 | # we try 3 times, backing off twice before succeeding | |
479 | assert len(log['success']) == 1 | |
480 | assert len(log['backoff']) == 2 | |
481 | assert len(log['giveup']) == 0 | |
482 | ||
483 | for i in range(2): | |
484 | details = log['backoff'][i] | |
485 | elapsed = details.pop('elapsed') | |
486 | assert isinstance(elapsed, float) | |
487 | assert details == {'args': (1, 2, 3), | |
488 | 'kwargs': {'foo': 1, 'bar': 2}, | |
489 | 'target': succeeder._target, | |
490 | 'tries': i + 1, | |
491 | 'wait': 0} | |
492 | ||
493 | details = log['success'][0] | |
494 | elapsed = details.pop('elapsed') | |
495 | assert isinstance(elapsed, float) | |
496 | assert details == {'args': (1, 2, 3), | |
497 | 'kwargs': {'foo': 1, 'bar': 2}, | |
498 | 'target': succeeder._target, | |
499 | 'tries': 3} | |
500 | ||
501 | ||
502 | # To maintain backward compatibility, | |
503 | # on_predicate should support 0-argument jitter function. | |
504 | @pytest.mark.asyncio | |
505 | async def test_on_predicate_success_0_arg_jitter(monkeypatch): | |
506 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
507 | monkeypatch.setattr('random.random', lambda: 0) | |
508 | ||
509 | log, log_success, log_backoff, log_giveup = _log_hdlrs() | |
510 | ||
511 | @backoff.on_predicate(backoff.constant, | |
512 | on_success=log_success, | |
513 | on_backoff=log_backoff, | |
514 | on_giveup=log_giveup, | |
515 | jitter=random.random, | |
516 | interval=0) | |
517 | @_save_target | |
518 | async def success(*args, **kwargs): | |
519 | # succeed after we've backed off twice | |
520 | return len(log['backoff']) == 2 | |
521 | ||
522 | with pytest.deprecated_call(): | |
523 | await success(1, 2, 3, foo=1, bar=2) | |
524 | ||
525 | # we try 3 times, backing off twice before succeeding | |
526 | assert len(log['success']) == 1 | |
527 | assert len(log['backoff']) == 2 | |
528 | assert len(log['giveup']) == 0 | |
529 | ||
530 | for i in range(2): | |
531 | details = log['backoff'][i] | |
532 | elapsed = details.pop('elapsed') | |
533 | assert isinstance(elapsed, float) | |
534 | assert details == {'args': (1, 2, 3), | |
535 | 'kwargs': {'foo': 1, 'bar': 2}, | |
536 | 'target': success._target, | |
537 | 'tries': i + 1, | |
538 | 'value': False, | |
539 | 'wait': 0} | |
540 | ||
541 | details = log['success'][0] | |
542 | elapsed = details.pop('elapsed') | |
543 | assert isinstance(elapsed, float) | |
544 | assert details == {'args': (1, 2, 3), | |
545 | 'kwargs': {'foo': 1, 'bar': 2}, | |
546 | 'target': success._target, | |
547 | 'tries': 3, | |
548 | 'value': True} | |
549 | ||
550 | ||
551 | @pytest.mark.asyncio | |
552 | async def test_on_exception_callable_max_tries(monkeypatch): | |
553 | monkeypatch.setattr('asyncio.sleep', _await_none) | |
554 | ||
555 | def lookup_max_tries(): | |
556 | return 3 | |
557 | ||
558 | log = [] | |
559 | ||
560 | @backoff.on_exception(backoff.constant, | |
561 | ValueError, | |
562 | max_tries=lookup_max_tries) | |
563 | async def exceptor(): | |
564 | log.append(True) | |
565 | raise ValueError() | |
566 | ||
567 | with pytest.raises(ValueError): | |
568 | await exceptor() | |
569 | ||
570 | assert len(log) == 3 | |
571 | ||
572 | ||
573 | @pytest.mark.asyncio | |
574 | async def test_on_exception_callable_gen_kwargs(): | |
575 | ||
576 | def lookup_foo(): | |
577 | return "foo" | |
578 | ||
579 | def wait_gen(foo=None, bar=None): | |
580 | assert foo == "foo" | |
581 | assert bar == "bar" | |
582 | ||
583 | while True: | |
584 | yield 0 | |
585 | ||
586 | @backoff.on_exception(wait_gen, | |
587 | ValueError, | |
588 | max_tries=2, | |
589 | foo=lookup_foo, | |
590 | bar="bar") | |
591 | async def exceptor(): | |
592 | raise ValueError("aah") | |
593 | ||
594 | with pytest.raises(ValueError): | |
595 | await exceptor() | |
596 | ||
597 | ||
598 | @pytest.mark.asyncio | |
599 | async def test_on_exception_coro_cancelling(event_loop): | |
600 | sleep_started_event = asyncio.Event() | |
601 | ||
602 | @backoff.on_predicate(backoff.expo) | |
603 | async def coro(): | |
604 | sleep_started_event.set() | |
605 | ||
606 | try: | |
607 | await asyncio.sleep(10) | |
608 | except asyncio.CancelledError: | |
609 | return True | |
610 | ||
611 | return False | |
612 | ||
613 | task = event_loop.create_task(coro()) | |
614 | ||
615 | await sleep_started_event.wait() | |
616 | ||
617 | task.cancel() | |
618 | ||
619 | assert (await task) | |
620 | ||
621 | ||
622 | def test_on_predicate_on_regular_function_without_event_loop(monkeypatch): | |
623 | monkeypatch.setattr('time.sleep', lambda x: None) | |
624 | ||
625 | # Set default event loop to None. | |
626 | loop = asyncio.get_event_loop() | |
627 | asyncio.set_event_loop(None) | |
628 | ||
629 | try: | |
630 | @backoff.on_predicate(backoff.expo) | |
631 | def return_true(log, n): | |
632 | val = (len(log) == n - 1) | |
633 | log.append(val) | |
634 | return val | |
635 | ||
636 | log = [] | |
637 | ret = return_true(log, 3) | |
638 | assert ret is True | |
639 | assert 3 == len(log) | |
640 | ||
641 | finally: | |
642 | # Restore event loop. | |
643 | asyncio.set_event_loop(loop) | |
644 | ||
645 | ||
646 | def test_on_exception_on_regular_function_without_event_loop(monkeypatch): | |
647 | monkeypatch.setattr('time.sleep', lambda x: None) | |
648 | ||
649 | # Set default event loop to None. | |
650 | loop = asyncio.get_event_loop() | |
651 | asyncio.set_event_loop(None) | |
652 | ||
653 | try: | |
654 | @backoff.on_exception(backoff.expo, KeyError) | |
655 | def keyerror_then_true(log, n): | |
656 | if len(log) == n: | |
657 | return True | |
658 | e = KeyError() | |
659 | log.append(e) | |
660 | raise e | |
661 | ||
662 | log = [] | |
663 | assert keyerror_then_true(log, 3) is True | |
664 | assert 3 == len(log) | |
665 | ||
666 | finally: | |
667 | # Restore event loop. | |
668 | asyncio.set_event_loop(loop) |
0 | """Integration tests | |
1 | ||
2 | Higher-level tests integrating with 3rd party modules using iodiomatic | |
3 | backoff patterns. | |
4 | """ | |
5 | ||
6 | import backoff | |
7 | ||
8 | ||
9 | import requests | |
10 | from requests import HTTPError | |
11 | import responses | |
12 | ||
13 | ||
14 | @responses.activate | |
15 | def test_on_predicate_runtime(monkeypatch): | |
16 | ||
17 | log = [] | |
18 | ||
19 | def sleep(seconds): | |
20 | log.append(seconds) | |
21 | ||
22 | monkeypatch.setattr("time.sleep", sleep) | |
23 | ||
24 | url = "http://example.com" | |
25 | ||
26 | responses.add(responses.GET, url, status=429, headers={"Retry-After": "1"}) | |
27 | responses.add(responses.GET, url, status=429, headers={"Retry-After": "3"}) | |
28 | responses.add(responses.GET, url, status=429, headers={"Retry-After": "7"}) | |
29 | responses.add(responses.GET, url, status=200) | |
30 | ||
31 | @backoff.on_predicate( | |
32 | backoff.runtime, | |
33 | predicate=lambda r: r.status_code == 429, | |
34 | value=lambda r: int(r.headers.get("Retry-After")), | |
35 | jitter=None, | |
36 | ) | |
37 | def get_url(): | |
38 | return requests.get(url) | |
39 | ||
40 | resp = get_url() | |
41 | assert resp.status_code == 200 | |
42 | ||
43 | assert log == [1, 3, 7] | |
44 | ||
45 | ||
46 | @responses.activate | |
47 | def test_on_exception_runtime(monkeypatch): | |
48 | ||
49 | log = [] | |
50 | ||
51 | def sleep(seconds): | |
52 | log.append(seconds) | |
53 | ||
54 | monkeypatch.setattr("time.sleep", sleep) | |
55 | ||
56 | url = "http://example.com" | |
57 | ||
58 | responses.add(responses.GET, url, status=429, headers={"Retry-After": "1"}) | |
59 | responses.add(responses.GET, url, status=429, headers={"Retry-After": "3"}) | |
60 | responses.add(responses.GET, url, status=429, headers={"Retry-After": "7"}) | |
61 | responses.add(responses.GET, url, status=200) | |
62 | ||
63 | @backoff.on_exception( | |
64 | backoff.runtime, | |
65 | HTTPError, | |
66 | value=lambda e: int(e.response.headers.get("Retry-After")), | |
67 | jitter=None, | |
68 | ) | |
69 | def get_url(): | |
70 | resp = requests.get(url) | |
71 | resp.raise_for_status() | |
72 | return resp | |
73 | ||
74 | resp = get_url() | |
75 | assert resp.status_code == 200 | |
76 | ||
77 | assert log == [1, 3, 7] |
3 | 3 | |
4 | 4 | def test_expo(): |
5 | 5 | gen = backoff.expo() |
6 | gen.send(None) | |
6 | 7 | for i in range(9): |
7 | assert 2**i == next(gen) | |
8 | assert 2 ** i == next(gen) | |
8 | 9 | |
9 | 10 | |
10 | 11 | def test_expo_base3(): |
11 | 12 | gen = backoff.expo(base=3) |
13 | gen.send(None) | |
12 | 14 | for i in range(9): |
13 | assert 3**i == next(gen) | |
15 | assert 3 ** i == next(gen) | |
14 | 16 | |
15 | 17 | |
16 | 18 | def test_expo_factor3(): |
17 | 19 | gen = backoff.expo(factor=3) |
20 | gen.send(None) | |
18 | 21 | for i in range(9): |
19 | assert 3 * 2**i == next(gen) | |
22 | assert 3 * 2 ** i == next(gen) | |
20 | 23 | |
21 | 24 | |
22 | 25 | def test_expo_base3_factor5(): |
23 | 26 | gen = backoff.expo(base=3, factor=5) |
27 | gen.send(None) | |
24 | 28 | for i in range(9): |
25 | assert 5 * 3**i == next(gen) | |
29 | assert 5 * 3 ** i == next(gen) | |
26 | 30 | |
27 | 31 | |
28 | 32 | def test_expo_max_value(): |
29 | gen = backoff.expo(max_value=2**4) | |
33 | gen = backoff.expo(max_value=2 ** 4) | |
34 | gen.send(None) | |
30 | 35 | expected = [1, 2, 4, 8, 16, 16, 16] |
31 | 36 | for expect in expected: |
32 | 37 | assert expect == next(gen) |
34 | 39 | |
35 | 40 | def test_fibo(): |
36 | 41 | gen = backoff.fibo() |
42 | gen.send(None) | |
37 | 43 | expected = [1, 1, 2, 3, 5, 8, 13] |
38 | 44 | for expect in expected: |
39 | 45 | assert expect == next(gen) |
41 | 47 | |
42 | 48 | def test_fibo_max_value(): |
43 | 49 | gen = backoff.fibo(max_value=8) |
50 | gen.send(None) | |
44 | 51 | expected = [1, 1, 2, 3, 5, 8, 8, 8] |
45 | 52 | for expect in expected: |
46 | 53 | assert expect == next(gen) |
48 | 55 | |
49 | 56 | def test_constant(): |
50 | 57 | gen = backoff.constant(interval=3) |
58 | gen.send(None) | |
51 | 59 | for i in range(9): |
52 | 60 | assert 3 == next(gen) |
61 | ||
62 | ||
63 | def test_runtime(): | |
64 | gen = backoff.runtime(value=lambda x: x) | |
65 | gen.send(None) | |
66 | for i in range(20): | |
67 | assert i == gen.send(i) |