This is a Python decorator which helps implementing an aspect oriented implementation of a retrying of certain steps which might fail sometimes. A typical example for this would be communication processes with the outside world, e. g. HTTP requests, allocation of some resource, etc. To use it, refactor the step in question into a (local) function and decorate this with the retry
decorator. See examples in the discussion sector below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import itertools
def retry(delays=(0, 1, 5, 30, 180, 600, 3600),
exception=Exception,
report=lambda *args: None):
def wrapper(function):
def wrapped(*args, **kwargs):
problems = []
for delay in itertools.chain(delays, [ None ]):
try:
return function(*args, **kwargs)
except exception as problem:
problems.append(problem)
if delay is None:
report("retryable failed definitely:", problems)
raise
else:
report("retryable failed:", problem,
"-- delaying for %ds" % delay)
time.sleep(delay)
return wrapped
return wrapper
|
Example
Consider a piece of code trying to send an HTTP request to an Internet server and expecting a meaningful response. Of course, if a network is involved, things can fail. You can run into a timeout, have a transmission problem and other things. A typical approach to tackle these issues is to just retry the HTTP request until it succeeds (or until a certain number of retries have all failed). Because an unreachable server might be an issue which is solved only in some considerable time (e. g. in a couple of hours when some operator has fixed the problem), it makes sense to have some (growing) delays between the retries.
Consider a code like this:
import requests
def send_data(data):
response = requests.post(URL, data=data)
return response.content
The requests.post()
call might fail (with an exception), and if so, should be repeated. To achieve this, you can change the existing code using the decorator given above:
import requests
def send_data(data):
@retry()
def send_post():
return requests.post(URL, data=data)
response = send_post()
return response.content
This will solve the issue with the default values. You can override the defaults:
delays
@retry(delays=itertools.cycle([ 20 ]))
The value for delays
must be an iterable which yields the delays in seconds to be waited between the retries. If the iterable is exhausted, no further repetitions are retried. The example above will result in an indefinite number of retries with 20 seconds delay between each. The default value is the tuple (0, 1, 5, 30, 180, 600, 3600)
which means that the first retry is done immediately (after 0 seconds), the next retry after 1 second, the next after 5, then 30, then 3 minutes, then 10 minutes, then 1 hour; if all these retries fail, no further repetitions are retried and the last exception is thrown.
exception
@retry(exception=(requests.exceptions.Timeout, requests.exceptions.ConnectionError))
The value for exception
can be a single exception class or a tuple of several. Only these exceptions are caught and can lead to a repetition in a retry; other exceptions will just be raised. The default value is just Exception
which means all normal exceptions lead to a retry; Error
s (like out of memory or keyboard interrupts) will not be caught.
report
@retry(report=print) # Python3!
The value for report
can be a callable which will be given some human readable information in the format as expected by print()
as known from Python3. The default value for report
is a NOP.