Welcome, guest | Sign In | My Account | Store | Cart

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.

Python, 22 lines
 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; Errors (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.