
I don't often write about tools I use when for my daily software development
tasks. I recently realized that I really should start to share more often my
workflows and weapons of choice.
One thing that I have a hard time enduring while doing Python code reviews, is
people writing utility code that is not directly tied to the core of their
business. This looks to me as wasted time maintaining code that should be
reused from elsewhere.
So today I'd like to start with
retrying, a Python package that you
can use to retry anything.
It's OK to fail
Often in computing, you have to deal with external resources. That means
accessing resources you don't control. Resources that can fail, become
flapping, unreachable or unavailable.
Most applications don't deal with that at all, and explode in flight, leaving a
skeptical user in front of the computer. A lot of software engineers refuse to
deal with failure, and don't bother handling this kind of scenario in their
code.
In the best case, applications usually handle simply the case where the
external reached system is out of order. They log something, and inform the
user that it should try again later.
In this cloud computing area, we tend to design software components with
service-oriented architecture
in mind. That means having a lot of different services talking to each others
over the network. And we all know that networks tend to fail, and distributed
systems too. Writing software with failing being part of normal operation is a
terrific idea.
Retrying
In order to help applications with the handling of these potential failures,
you need a plan. Leaving to the user the burden to "try again later" is rarely
a good choice. Therefore, most of the time you want your application to
retry.
Retrying an action is a full strategy on its own, with a lot of options. You
can retry only on certain condition, and with the number of tries based on time
(e.g. every second), based on a number of tentative (e.g. retry 3 times and
abort), based on the problem encountered, or even on all of those.
For all of that, I use the
retrying
library that you can retrieve easily on
PyPI.
retrying provides a decorator called
retry that you can use on top of any
function or method in Python to make it retry in case of failure. By default,
retry calls your function endlessly until it returns rather than raising an
error.
import random
from retrying import retry
@retry
def pick_one():
if random.randint(0, 10) != 1:
raise Exception("1 was not picked")
This will execute the function
pick_one until
1 is returned by
random.randint.
retry accepts a few arguments, such as the minimum and maximum delays to use,
which also can be randomized. Randomizing delay is a good strategy to avoid
detectable pattern or congestion. But more over, it supports exponential delay,
which can be used to implement
exponential backoff, a
good solution for retrying tasks while really avoiding congestion. It's
especially handy for background tasks.
@retry(wait_exponential_multiplier=1000, wait_exponential_max=10000)
def wait_exponential_1000():
print "Wait 2^x * 1000 milliseconds between each retry, up to 10 seconds, then 10 seconds afterwards"
raise Exception("Retry!")
You can mix that with a maximum delay, which can give you a good strategy to
retry for a while, and then fail anyway:
# Stop retrying after 30 seconds anyway
>>> @retry(wait_exponential_multiplier=1000, wait_exponential_max=10000, stop_max_delay=30000)
... def wait_exponential_1000():
... print "Wait 2^x * 1000 milliseconds between each retry, up to 10 seconds, then 10 seconds afterwards"
... raise Exception("Retry!")
...
>>> wait_exponential_1000()
Wait 2^x * 1000 milliseconds between each retry, up to 10 seconds, then 10 seconds afterwards
Wait 2^x * 1000 milliseconds between each retry, up to 10 seconds, then 10 seconds afterwards
Wait 2^x * 1000 milliseconds between each retry, up to 10 seconds, then 10 seconds afterwards
Wait 2^x * 1000 milliseconds between each retry, up to 10 seconds, then 10 seconds afterwards
Wait 2^x * 1000 milliseconds between each retry, up to 10 seconds, then 10 seconds afterwards
Wait 2^x * 1000 milliseconds between each retry, up to 10 seconds, then 10 seconds afterwards
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/lib/python2.7/site-packages/retrying.py", line 49, in wrapped_f
return Retrying(*dargs, **dkw).call(f, *args, **kw)
File "/usr/local/lib/python2.7/site-packages/retrying.py", line 212, in call
raise attempt.get()
File "/usr/local/lib/python2.7/site-packages/retrying.py", line 247, in get
six.reraise(self.value[0], self.value[1], self.value[2])
File "/usr/local/lib/python2.7/site-packages/retrying.py", line 200, in call
attempt = Attempt(fn(*args, **kwargs), attempt_number, False)
File "<stdin>", line 4, in wait_exponential_1000
Exception: Retry!
A pattern I use very often, is the ability to retry only based on some
exception type. You can specify a function to filter out exception you want to
ignore or the one you want to use to retry.
def retry_on_ioerror(exc):
return isinstance(exc, IOError)
@retry(retry_on_exception=retry_on_ioerror)
def read_file():
with open("myfile", "r") as f:
return f.read()
retry will call the function passed as
retry_on_exception with the
exception raised as first argument. It's up to the function to then return a
boolean indicating if a retry should be performed or not. In the example above,
this will only retry to read the file if an
IOError occurs; if any other
exception type is raised, no retry will be performed.
The same pattern can be implemented using the keyword argument
retry_on_result, where you can provide a function that analyses the result
and retry based on it.
def retry_if_file_empty(result):
return len(result) <= 0
@retry(retry_on_result=retry_if_file_empty)
def read_file():
with open("myfile", "r") as f:
return f.read()
This example will read the file until it stops being empty. If the file does
not exist, an
IOError is raised, and the default behavior which triggers
retry on all exceptions kicks-in the retry is therefore performed.
That's it!
retry is really a good and small library that you should leverage
rather than implementing your own half-baked solution!