Search code examples
pythonpython-3.xpython-requestsurllib

Best practices in repeating request if request failed?


I'm curious about best practices in repeating a failed request using the requests library in Python3. I have a simple API wrapper that makes a get request to a constructed URL. If an exception is raised by requests, I'd like to retry the request before raising an exception.

I'm unsure if there's some standard practice that I'm unaware of. What I have will repeat the request 10x, doubling the wait time between requests in each iteration.

import time
import requests
from requests.exceptions import RequestException


def get_request(*args, max_retry=10, **kwargs):
    """ Gets requests.models.Response object using requests.get.
    Retry request if request fails, with number of iteration specified
    by max_retry. """

    def recurse_get_request(*args, retries=0, wait=0.005, **kwargs):
        try:
            return requests.get(*args, **kwargs)
        except RequestException as exc:
            if retries > max_retry:
                raise RequestException from exc

            print("Request failed: (%s). Retrying after %s seconds ..." % (exc, "%.2f" % wait))
            time.sleep(wait)
            # double the wait time after every iteration
            wait *= 2
            retries += 1
            return recurse_get_request(*args, retries=retries, wait=wait, **kwargs)

    return recurse_get_request(*args, **kwargs)


get_request('https://sfbay.craigs.org')  # bad url

Solution

  • What you have is called truncated exponential backoff, and it's quite good already. It's called "truncated" because it eventually stops retrying and gives up entirely.

    The linked description on Wikipedia also implements randomization, which I would call truncated randomized exponential backoff. Randomization is not necessary if there's only a single client, but if there are multiple clients competing for the same resource using the same backoff schedule, you could run into the thundering herd problem. Randomization helps to avoid this.

    The Python package Tenacity helps you implement all this easily with a nice unobtrusive decorator. Here's an example of how I'm using it:

    import tenacity
    
    # Retry 10 times, starting with 1 second and doubling the delay every time.
    _RETRY_ARGS = {
        'wait': tenacity.wait.wait_random_exponential(multiplier=1.0, exp_base=2),
        'stop': tenacity.stop.stop_after_attempt(10)
    }
    
    @tenacity.retry(**_RETRY_ARGS)
    def something_that_might_fail():
        ...
    
    @tenacity.retry(**_RETRY_ARGS)
    def something_else_that_might_fail():
        ...