Codebase list backoff / fresh-releases/upstream
Import upstream version 2.0.1 Kali Janitor 2 years ago
24 changed file(s) with 1277 addition(s) and 1029 deletion(s). Raw diff Collapse all Expand all
+0
-8
.coveragerc-py2 less more
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
-2
.coveragerc-py35 less more
0 [report]
1 show_missing = True
88 *.egg-info
99 poetry.lock
1010 .vscode
11 .python-version
00 language: python
11 python:
2 - "2.7"
3 - "3.5"
4 - "3.6"
52 - "3.7"
63 - "3.8"
74 - "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"
216 before_install:
227 - pip install poetry more-itertools
238 install:
0 # Change Log
0 # Changelog
11
2 ## [v1.0.3] - 2014-06-05
2 ## [v2.0.1] - 2022-04-27
33 ### Changed
4 - Make logging unicode safe
5 - Log on_predicate backoff as INFO rather than ERROR
4 - Allow None for jitter keyword arg (typing)
65
7 ## [v1.0.4] - 2014-08-12
6
7 ## [v2.0.0] - 2022-04-26
88 ### 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
1218
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
1426 ### 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
1728
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
1952 ### 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
2158
59 ## [v1.7.0] - 2018-11-23
2260 ### 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
2466
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
2671
72 ## [v1.5.0] - 2018-04-11
73 ### Added
74 - Add max_time keyword argument
75
76 ## [v1.4.3] - 2017-05-22
2777 ### Changed
28 - Fix string formatting for python 2.6
78 - Add license to source distribution
2979
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
3186 ### 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
3491
92 ## [v1.4.0] - 2017-02-05
93 ### Added
94 - Async support via `asyncio` coroutines (Python 3.4) from @rutsky
3595 ### 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
38118
39119 ## [v1.2.0] - 2016-05-26
40120 ### Added
45125 - Change README to reST for the benefit of pypi :(
46126 - Remove docstring doc generation and make README canonical
47127
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
49132 ### Changed
50 - Documentation fixes
133 - Docs and test for multi exception invocations
134 - Update dev environment test dependencies
51135
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
53141 ### 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
56145
146 ## [v1.0.5] - 2015-02-03
57147 ### 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
59150
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
61158 ### 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
1212 @echo 'check make sure you are ready to commit'
1313
1414 flake8:
15 ifeq ($(PY_GTE_35),1)
1615 @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
2019
2120 clean:
2221 @find . -name "*.pyc" -delete
2423 @rm -rf build dist .coverage MANIFEST
2524
2625 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
3227
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; }
00 backoff
11 =======
22
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
79 .. image:: https://img.shields.io/pypi/v/backoff.svg
810 :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
913
1014 **Function decoration for backoff and retry**
1115
1721 polling resources for externally generated content.
1822
1923 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
2125 for asynchronous code.
2226
2327 Examples
101105 def get_url(url):
102106 return requests.get(url)
103107
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
105109 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.
107133
108134 @backoff.on_predicate
109135 ---------------------
131157 .. code-block:: python
132158
133159 @backoff.on_predicate(backoff.fibo, max_value=13)
134 def poll_for_message(queue)
160 def poll_for_message(queue):
135161 return queue.get()
136162
137163 More simply, a function which continues polling every second until it
140166 .. code-block:: python
141167
142168 @backoff.on_predicate(backoff.constant, interval=1)
143 def poll_for_message(queue)
169 def poll_for_message(queue):
144170 return queue.get()
145171
146172 Jitter
179205 max_time=300)
180206 def poll_for_message(queue):
181207 return queue.get()
208
182209
183210 Runtime Configuration
184211 ---------------------
266293 Backoff supports asynchronous execution in Python 3.5 and above.
267294
268295 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>`__
270297 you simply need to apply ``backoff.on_exception`` or ``backoff.on_predicate``
271298 to coroutines.
272299 You can also use coroutines for the ``on_success``, ``on_backoff``, and
273300 ``on_giveup`` event handlers, with the interface otherwise being identical.
274301
275 The following examples use `aiohttp <https://aiohttp.readthedocs.io/>`_
302 The following examples use `aiohttp <https://aiohttp.readthedocs.io/>`__
276303 asynchronous HTTP client/server library.
277304
278305 .. code-block:: python
1111 For examples and full documentation see the README at
1212 https://github.com/litl/backoff
1313 """
14 import sys
15 import warnings
16
1714 from backoff._decorator import on_predicate, on_exception
1815 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
2017
2118 __all__ = [
2219 'on_predicate',
2421 'constant',
2522 'expo',
2623 'fibo',
24 'runtime',
2725 'full_jitter',
2826 'random_jitter',
2927 ]
3028
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'
00 # coding:utf-8
11 import datetime
22 import functools
3 import asyncio # Python 3.5 code and syntax is allowed in this file
3 import asyncio
44 from datetime import timedelta
55
66 from backoff._common import (_init_wait_gen, _maybe_call, _next_wait)
2020 return [_ensure_coroutine(f) for f in coros_or_funcs]
2121
2222
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):
2427 details = {
2528 'target': target,
2629 'args': args,
2932 'elapsed': elapsed,
3033 }
3134 details.update(extra)
32 for hdlr in hdlrs:
33 await hdlr(details)
35 for handler in handlers:
36 await handler(details)
3437
3538
3639 def retry_predicate(target, wait_gen, predicate,
40 *,
3741 max_tries, max_time, jitter,
3842 on_success, on_backoff, on_giveup,
3943 wait_gen_kwargs):
5054 @functools.wraps(target)
5155 async def retry(*args, **kwargs):
5256
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)
5661
5762 tries = 0
5863 start = datetime.datetime.now()
6065 while True:
6166 tries += 1
6267 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 }
6475
6576 ret = await target(*args, **kwargs)
6677 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)
7081
7182 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)
7384 break
7485
7586 try:
76 seconds = _next_wait(wait, jitter, elapsed, max_time_)
87 seconds = _next_wait(wait, ret, jitter, elapsed, max_time)
7788 except StopIteration:
78 await _call_handlers(on_giveup, *details, value=ret)
89 await _call_handlers(on_giveup, **details, value=ret)
7990 break
8091
81 await _call_handlers(on_backoff, *details, value=ret,
92 await _call_handlers(on_backoff, **details, value=ret,
8293 wait=seconds)
8394
8495 # Note: there is no convenient way to pass explicit event
93104 await asyncio.sleep(seconds)
94105 continue
95106 else:
96 await _call_handlers(on_success, *details, value=ret)
107 await _call_handlers(on_success, **details, value=ret)
97108 break
98109
99110 return ret
102113
103114
104115 def retry_exception(target, wait_gen, exception,
116 *,
105117 max_tries, max_time, jitter, giveup,
106 on_success, on_backoff, on_giveup,
118 on_success, on_backoff, on_giveup, raise_on_giveup,
107119 wait_gen_kwargs):
108120 on_success = _ensure_coroutines(on_success)
109121 on_backoff = _ensure_coroutines(on_backoff)
116128
117129 @functools.wraps(target)
118130 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)
122136
123137 tries = 0
124138 start = datetime.datetime.now()
126140 while True:
127141 tries += 1
128142 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 }
130150
131151 try:
132152 ret = await target(*args, **kwargs)
133153 except exception as e:
134154 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)
138158
139159 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
142164
143165 try:
144 seconds = _next_wait(wait, jitter, elapsed, max_time_)
166 seconds = _next_wait(wait, e, jitter, elapsed, max_time)
145167 except StopIteration:
146 await _call_handlers(on_giveup, *details)
168 await _call_handlers(on_giveup, **details)
147169 raise e
148170
149 await _call_handlers(on_backoff, *details, wait=seconds)
171 await _call_handlers(on_backoff, **details, wait=seconds)
150172
151173 # Note: there is no convenient way to pass explicit event
152174 # loop to decorator, so here we assume that either default
159181 # <https://bugs.python.org/issue28613>
160182 await asyncio.sleep(seconds)
161183 else:
162 await _call_handlers(on_success, *details)
184 await _call_handlers(on_success, **details)
163185
164186 return ret
165187 return retry
44 import sys
55 import traceback
66 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
147
158
169 # Use module-specific logger with a default null handler.
2114
2215 # Evaluate arg that can be either a fixed value or a callable.
2316 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
2524
2625
2726 def _init_wait_gen(wait_gen, wait_gen_kwargs):
2827 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
3031
3132
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)
3435 try:
3536 if jitter is not None:
3637 seconds = jitter(value)
5556
5657
5758 def _prepare_logger(logger):
58 if isinstance(logger, basestring):
59 if isinstance(logger, str):
5960 logger = logging.getLogger(logger)
6061 return logger
6162
6364 # Configure handler list with user specified handler and optionally
6465 # with a default handler bound to the specified logger.
6566 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
6768 ):
6869 handlers = []
6970 if logger is not None:
00 # coding:utf-8
1 from __future__ import unicode_literals
2
1 import asyncio
32 import logging
43 import operator
5 import sys
4 from typing import Any, Callable, Iterable, Optional, Type, Union
65
76 from backoff._common import (
87 _prepare_logger,
1110 _log_giveup
1211 )
1312 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]:
2939 """Returns decorator for backoff and retry triggered by predicate.
3040
3141 Args:
7080 This is useful for runtime configuration.
7181 """
7282 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:
93103 retry = _sync.retry_predicate
94104
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 )
99117
100118 # Return a function which decorates a target with a retry loop.
101119 return decorate
102120
103121
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]:
117137 """Returns decorator for backoff and retry triggered by exception.
118138
119139 Args:
149169 signature to be called in the event that max_tries
150170 is exceeded. The parameter is a dict containing details
151171 about the invocation.
172 raise_on_giveup: Boolean indicating whether the registered exceptions
173 should be raised on giveup. Defaults to `True`
152174 logger: Name or Logger object to log to. Defaults to 'backoff'.
153175 backoff_log_level: log level for the backoff event. Defaults to "INFO"
154176 giveup_log_level: log level for the give up event. Defaults to "ERROR"
158180 This is useful for runtime configuration.
159181 """
160182 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:
181203 retry = _sync.retry_exception
182204
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 )
187219
188220 # Return a function which decorates a target with a retry loop.
189221 return decorate
22 import random
33
44
5 def random_jitter(value):
5 def random_jitter(value: float) -> float:
66 """Jitter the value a random number of milliseconds.
77
88 This adds up to 1 second of additional time to the original value.
1414 return value + random.random()
1515
1616
17 def full_jitter(value):
17 def full_jitter(value: float) -> float:
1818 """Jitter the value across the full range (0 to value).
1919
2020 This corresponds to the "Full Jitter" algorithm specified in the
2020
2121
2222 def retry_predicate(target, wait_gen, predicate,
23 *,
2324 max_tries, max_time, jitter,
2425 on_success, on_backoff, on_giveup,
2526 wait_gen_kwargs):
2728 @functools.wraps(target)
2829 def retry(*args, **kwargs):
2930
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)
3335
3436 tries = 0
3537 start = datetime.datetime.now()
3739 while True:
3840 tries += 1
3941 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 }
4149
4250 ret = target(*args, **kwargs)
4351 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)
4755
4856 if max_tries_exceeded or max_time_exceeded:
49 _call_handlers(on_giveup, *details, value=ret)
57 _call_handlers(on_giveup, **details, value=ret)
5058 break
5159
5260 try:
53 seconds = _next_wait(wait, jitter, elapsed, max_time_)
61 seconds = _next_wait(wait, ret, jitter, elapsed, max_time)
5462 except StopIteration:
55 _call_handlers(on_giveup, *details)
63 _call_handlers(on_giveup, **details)
5664 break
5765
58 _call_handlers(on_backoff, *details,
66 _call_handlers(on_backoff, **details,
5967 value=ret, wait=seconds)
6068
6169 time.sleep(seconds)
6270 continue
6371 else:
64 _call_handlers(on_success, *details, value=ret)
72 _call_handlers(on_success, **details, value=ret)
6573 break
6674
6775 return ret
7078
7179
7280 def retry_exception(target, wait_gen, exception,
81 *,
7382 max_tries, max_time, jitter, giveup,
74 on_success, on_backoff, on_giveup,
83 on_success, on_backoff, on_giveup, raise_on_giveup,
7584 wait_gen_kwargs):
7685
7786 @functools.wraps(target)
7887 def retry(*args, **kwargs):
7988
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)
8393
8494 tries = 0
8595 start = datetime.datetime.now()
8797 while True:
8898 tries += 1
8999 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 }
91107
92108 try:
93109 ret = target(*args, **kwargs)
94110 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)
98114
99115 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
102120
103121 try:
104 seconds = _next_wait(wait, jitter, elapsed, max_time_)
122 seconds = _next_wait(wait, e, jitter, elapsed, max_time)
105123 except StopIteration:
106 _call_handlers(on_giveup, *details)
124 _call_handlers(on_giveup, **details)
107125 raise e
108126
109 _call_handlers(on_backoff, *details, wait=seconds)
127 _call_handlers(on_backoff, **details, wait=seconds)
110128
111129 time.sleep(seconds)
112130 else:
113 _call_handlers(on_success, *details)
131 _call_handlers(on_success, **details)
114132
115133 return ret
116134 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]]
00 # coding:utf-8
11
22 import itertools
3 from typing import Any, Callable, Generator, Iterable, Optional, Union
34
45
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
612 """Generator for exponential decay.
713
814 Args:
1218 true exponential sequence exceeds this, the value
1319 of max_value will forever after be yielded.
1420 """
21 # Advance past initial .send() call
22 yield # type: ignore[misc]
1523 n = 0
1624 while True:
1725 a = factor * base ** n
2230 yield max_value
2331
2432
25 def fibo(max_value=None):
33 def fibo(max_value: Optional[int] = None) -> Generator[int, None, None]:
2634 """Generator for fibonaccial decay.
2735
2836 Args:
3038 true fibonacci sequence exceeds this, the value
3139 of max_value will forever after be yielded.
3240 """
41 # Advance past initial .send() call
42 yield # type: ignore[misc]
43
3344 a = 1
3445 b = 1
3546 while True:
4051 yield max_value
4152
4253
43 def constant(interval=1):
54 def constant(
55 interval: Union[int, Iterable[int]] = 1
56 ) -> Generator[int, None, None]:
4457 """Generator for constant intervals.
4558
4659 Args:
4760 interval: A constant value to yield or an iterable of such values.
4861 """
62 # Advance past initial .send() call
63 yield # type: ignore[misc]
64
4965 try:
50 itr = iter(interval)
66 itr = iter(interval) # type: ignore
5167 except TypeError:
52 itr = itertools.repeat(interval)
68 itr = itertools.repeat(interval) # type: ignore
5369
5470 for val in itr:
5571 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)
(New empty file)
0 # coding:utf-8
1 from ._typing import Details
2
3 __all__ = [
4 'Details'
5 ]
00 [tool.poetry]
11 name = "backoff"
2 version = "1.11.1"
2 version = "2.0.1"
33 description = "Function decoration for backoff and retry"
44 authors = ["Bob Green <[email protected]>"]
55 readme = "README.rst"
1313 'Natural Language :: English',
1414 'Operating System :: OS Independent',
1515 'Programming Language :: Python',
16 'Programming Language :: Python :: 2',
17 'Programming Language :: Python :: 2.7',
1816 'Programming Language :: Python :: 3',
19 'Programming Language :: Python :: 3.5',
20 'Programming Language :: Python :: 3.6',
2117 'Programming Language :: Python :: 3.7',
2218 'Programming Language :: Python :: 3.8',
2319 'Programming Language :: Python :: 3.9',
24 'Topic :: Internet :: WWW/HTTP',
20 'Programming Language :: Python :: 3.10',
21 'Topic :: Internet :: WWW/HTTP',
2522 'Topic :: Software Development :: Libraries :: Python Modules',
2623 'Topic :: Utilities']
2724 packages = [
2926 ]
3027
3128 [tool.poetry.dependencies]
32 python = "^2.7 || ^3.5"
29 python = "^3.7"
3330
3431 [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"
4040
4141 [build-system]
42 requires = ["poetry-core"]
42 requires = ["poetry-core>=1.0.0"]
4343 build-backend = "poetry.core.masonry.api"
+0
-664
tests/python35/test_backoff_async.py less more
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)
259259 'tries': 3}
260260
261261
262 def test_on_exception_giveup():
262 @pytest.mark.parametrize('raise_on_giveup', [True, False])
263 def test_on_exception_giveup(raise_on_giveup):
263264 backoffs, giveups, successes = [], [], []
264265
265266 @backoff.on_exception(backoff.constant,
269270 on_giveup=giveups.append,
270271 max_tries=3,
271272 jitter=None,
273 raise_on_giveup=raise_on_giveup,
272274 interval=0)
273275 @_save_target
274276 def exceptor(*args, **kwargs):
275277 raise ValueError("catch me")
276278
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:
278283 exceptor(1, 2, 3, foo=1, bar=2)
279284
280285 # 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]
0 # coding:utf-8
1
2 from backoff.types import Details
3
4
5 assert Details
33
44 def test_expo():
55 gen = backoff.expo()
6 gen.send(None)
67 for i in range(9):
7 assert 2**i == next(gen)
8 assert 2 ** i == next(gen)
89
910
1011 def test_expo_base3():
1112 gen = backoff.expo(base=3)
13 gen.send(None)
1214 for i in range(9):
13 assert 3**i == next(gen)
15 assert 3 ** i == next(gen)
1416
1517
1618 def test_expo_factor3():
1719 gen = backoff.expo(factor=3)
20 gen.send(None)
1821 for i in range(9):
19 assert 3 * 2**i == next(gen)
22 assert 3 * 2 ** i == next(gen)
2023
2124
2225 def test_expo_base3_factor5():
2326 gen = backoff.expo(base=3, factor=5)
27 gen.send(None)
2428 for i in range(9):
25 assert 5 * 3**i == next(gen)
29 assert 5 * 3 ** i == next(gen)
2630
2731
2832 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)
3035 expected = [1, 2, 4, 8, 16, 16, 16]
3136 for expect in expected:
3237 assert expect == next(gen)
3439
3540 def test_fibo():
3641 gen = backoff.fibo()
42 gen.send(None)
3743 expected = [1, 1, 2, 3, 5, 8, 13]
3844 for expect in expected:
3945 assert expect == next(gen)
4147
4248 def test_fibo_max_value():
4349 gen = backoff.fibo(max_value=8)
50 gen.send(None)
4451 expected = [1, 1, 2, 3, 5, 8, 8, 8]
4552 for expect in expected:
4653 assert expect == next(gen)
4855
4956 def test_constant():
5057 gen = backoff.constant(interval=3)
58 gen.send(None)
5159 for i in range(9):
5260 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)