Getting Started

Warning

if you’re using asyncio, read the last section first.

1. Trying again

Let’s say we have to query a buggy webserver, which, once in a while, resets the client connection.

Let’s say, when that happens, we want to immediately re-perform the same request to that server.

To do that easily, we would just have to use the retry() decorator, when defining the function.

Basic usage (1): try again.
 from kaioretry import retry

 @retry(tries=2)
 def query_some_buggy_server(url):
     ...

This way, if the query_some_buggy_server function raises an exception, the decorator will immediately call query_some_buggy_server again.

Since we specified tries=2, then a single new try will be performed in case of failure. (It’s tries=2 does not mean retries=2)

2. Trying until it works

Now, let’s hypothesize that the server is reeeeeaaally buggy, but you know it randomly works.

Your solution here is to retry until it works, no matter how many attempts it takes.

Basic usage (1): try until it works.
 from kaioretry import retry

 # tries=-1 is the default. For the same purpose,
 # any negative value would do.
 @retry(tries=-1)
 def query_some_buggy_server(url):
     ...

Note

tries default value is -1. It means that by default retry will call the function again and again….. and agaaaaaaain, until it succeeds.

3. Catching specific exceptions

For the sake of continuity, let’s now consider the fact, that you know that, when the server fails, a ConnectionError is raised, and nothing else.

You would be, then, well advised not to retry, on, say, a KeyError, which would likely represent a flaw in your own code, like typo, or a (in)valid answer you did not expect.

To achieve this, you would have to specify the exception class that will trigger another try, by using the exceptions parameter.

Exceptions (1): only retry on a specific error.
 from requests import ConnectionError

 # Remember, tries=-1 is the default, so, even in explicit is better than
 # implicit, you do not have to repeat default values.
 @retry(exceptions=ConnectionError)
 def query_some_buggy_server(url):
     ...

Note

The exceptions parameter default value is Exception, which means that KaioRetry will loop for any error encountered by the decorated function.

Now, if you discover that the buggy server also generates some time out… Then brace yourself, and just add it to the exceptions parameter value.

Exceptions (2): only retry on some specific errors
 from requests import ConnectionError, ReadTimeout

 @retry(exceptions=(ConnectionError, ReadTimeout))
 def query_that_damn_server(url):
     ...

4. Adding a delay between tries

This being said, it is, I think, most of the time, advisable to wait a bit between attempting again, after a failure. We dont want to spam to death an already sick server, do we? This is made possible through the the delay parameter.

Let’s introduce a 2 seconds delay between each try, by using the delay parameter.

Basic Usage (3): only
 from requests import ConnectionError

 @retry(exceptions=ConnectionError, tries=2, delay=1)
 def query_that_damn_server(url):
     ...

Note

delay value is expressed in seconds. Either whole seconds (int) or whole seconds-and-then-some (float).

Note

delay default value is 0, which means no wait between tries.

Warning

delay cannot be negative, for obvious reasons. (like breaking the space-time continuum)

5. Increasing logs to analyse the retry process

You can actually check the time spent waiting between each tries, by simply increasing the log level.

So from the previous example:

Increase verbosity
 >>> logging.basicConfig(stream=sys.stdout, encoding='utf-8', level=logging.DEBUG)
 >>> query_that_damn_server()
 Retry(ConnectionError, Context(tries=2, delay=(0<=(1+0)*1<=None))): ConnectionError caught while running query_that_damn_server: reset by peer
 5c474e70-2d0d-44dd-90a9-745d9a21bb2e: 1 tries remaining
 5c474e70-2d0d-44dd-90a9-745d9a21bb2e: sleeping 1 seconds
 Retry(ConnectionError, Context(tries=2, delay=(0<=(1+0)*1<=None))): ConnectionError caught while running query_that_damn_server: reset by peer
 Retry(ConnectionError, Context(tries=2, delay=(0<=(1+0)*1<=None))): query_that_damn_server failed to complete

Note

you can set your own Logger by using the logger parameter.

Note

The uuid in the log lines will change every time the decorated version of the function is called, allowing you to keep track of the retry series.

6. Non-constant delay

If you want to increase, bit by bit, the delay value after each try, you give a non-zero value to the jitter parameter.

For instance, if we want the delay between tries to be 1 second, then 2 seconds, then 3, etc. then we will set an initial value of 1 (delay=1) and an increase value of 1 (jitter=1).

Basic delay: 1, 2, 3, 4, 5, 6…
 from requests import ConnectionError

 @retry(exceptions=ConnectionError, tries=10, delay=1, jitter=1)
 def query_that_damn_server(url):
     ...

Note

jitter default value is 0, which means that, by default, delay stays put and keep its initial value.

Note

Also, note that while jitter is permitted to be negative (which would imply delay becoming smaller and smaller), delay will internally be kept positive.

7. Exponential delay increase!

Another way to alter delay between each call is to use the backoff parameter. delay will be multiplied by backoff.

Basic delay: 1, 2, 4, 8, 16, 32…
 from requests import ConnectionError

 @retry(exceptions=ConnectionError, tries=10, delay=1, backoff=2)
 def query_that_damn_server(url):
     ...

Note

backoff default value is 1, which means by default, things stay the same.

Note

Also, it is possible to set backoff to a float value.

Note

Also also, it is also possible to set backoff to a value between 0 and 1, which would make delay shrink after each try.

Note

ALSO also also, combinations of jitter and backoff are permitted. backoff will multiply delay before jitter is added.

Warning

As previously reminded, at run time, delay value will never be less than zero.

8. Setting boundaries

Two extra parameters are available to control delay: min_delay and max_delay. If max_delay is set then, it will become the upper limit for delay value. The min_delay parameter is the lower limit of delay and delay will never be updated to less than min_delay

min’n’max.
 from requests import ConnectionError

 @retry(exceptions=ConnectionError, tries=10, delay=1, backoff=2,
        min_delay=1, max_delay=10)
 def query_that_damn_server(url):
     ...

Note

If max_delay is unset or None, and if you’re not careful, then… things could take a while to complete.

Note

Consistently with delay’s own constraints, min_delay cannot be set to a negative number.

9. AsyncIOchronously doing stuff

Let’s say you’re a smart cookie and you use the asyncio framework everywhere (just like I do). You know that, for that purpose, using a synchronous decorator over a coroutine function will not work. Maybe you’ve experienced it already (just like I have).

So you want an asyncIO-friendly retry decorator, without changing too much of your code?

Madame, Monsieur, Others, voila:

The aioretry() decorator!

aioretry basic usage
from aiohttp import ClientConnectionError
from kaioretry import aioretry                 # And voila

@aioretry(exceptions=ClientConnectionError)    # Tadaaa
async def my_fantabulous_but_error_raising_coroutine():
    ...

The aioretry() decorator produces coroutine functions. Besides that, it will work exactly like retry(): it accepts the same parameters, performs the same internal magic.

Note

aioretry() uses asyncio.sleep() instead of time.sleep(). Duh.

10. Regular/Sync functions in an AsyncIO context

Note

TL;DR: Always use aioretry() in an AsyncIO context. Even for regular functions. aioretry() will turn regular functions into coroutine functions and you will have to await them.

Warning

You should never use retry() in an asyncio context. Even for for regular (non-coroutines) functions.

Warning

Never.

Warning

This is a warning box abuse, right?

Anyway. “Why?” will you ask. It’s quite simple. time.sleep() also freezes the event loop.

You see, AsyncIO is a cooperative, event-driven framework.

It’s cooperative, because every time a coroutine function does an await on something, what it does in fact is notifying the scheduler (the event loop), in a friendly way, that it can give the priority to something else, for now.

By calling time.sleep() in a coroutine function, you will prevent said coroutine function to perform the next await. During the time it sleeps, the coroutine function will not be able to hand over to the event loop. Thus freezing the whole scheduling process. Not the best way to cooperate, if you ask me.

That’s why AsyncIO comes with its own sleep primitive, asyncio.sleep(), which is awaitable.

aioretry() will work on a regular function… but it will turn it into a coroutine function, and you will have to await it. In return it will not freeze your process.

Sounds fair? Sounded fair enough to me when I wrote that.

e.g: consider these 2 stupid functions
from kaioretry import retry, aioretry

@retry
def f():
    return 1

@aioretry()
def g():
    return 1
Now if we run them…
>>> f()
1
>>> # f is as stupid as you can guess.
>>> g()
<coroutine object g at 0x7f3dab722bd0>
>>> # g has become a coroutine function, though.
>>> # We have to await it,
>>> # Or feed it to asyncio.run.
>>> asyncio.run(g())
1

I hope this is not too confusing for you. Good luck. :]

Let me know if you can explain this better. Pull requests are always welcome.

Warning

if you came here from the very top of the page and dont know where to start, you should go back to the top.