Python retry!
In programming it's quite often you need to wait for an action to complete or some service availability. There are many ways and tools which able to do that. I would like to tell about two python libraries I've worked with.
wait_for
It's a small library originally created by my colleague Pete Savage
@psav. wait_for
is heavily used in web UI tests. When you click some
button you not always get the result immediately. Request processing can take some time. Some
application will show the result after page refresh, more modern applications use XHR and tons of
scripts 🤑 to update the page. In both cases you cannot just perform button clicking in the python
code. You have to wait until the result appears and only after that continue execution. Let me
provide some examples of usage:
view.submit_button.click()
wait_for(
lambda: view.flash.is_displayed,
delay=10,
timeout=120
)
view.flash.assert_no_error()
Here test automation clicks a button and then wait for another element appearing in the UI.
wait_for
just executes lambda: view.flash.is_displayed
over and over again until result will be
True
or time out.
wait_for
has a nice decorator:
@wait_for_decorator(delay=10, timeout=300)
def volume_type_is_displayed():
volume_type.refresh()
return volume_type.exists
tenacity
Everything gets a lot more fun when you start to use asyncio
. First of all you'll find out that
time.sleep()
blocks the main thread and you cannot use it in your asynchronous code. But wait
wait_for
uses time.sleep()
underneath. I wanted
to add asyncio
support but before I started I decided to search if someone already did it (yeah
I'm lazy I know). This is how I discovered tenacity
(credits to
Julien Danjou). You can use it in both synchronous and
asynchronous code under the same API. My task was to upload a payload to a server and wait for the
result from some REST API endpoint. But I wanted to upload a lot of payloads and check the results
after that. tenacity
and asyncio
fit perfectly for that task. Here is an example:
import asyncio
from tenacity import retry
from tenacity import retry_if_exception_type
from tenacity import retry_if_result
from tenacity import stop_after_delay
from tenacity import wait_fixed
@retry(
stop=stop_after_delay(300),
wait=wait_fixed(4),
retry_error_callback=lambda retry_state: False,
retry=(retry_if_result(lambda value: value is False) | retry_if_exception_type(Exception)),
)
async def find_host(session, hostname):
url = f"https://example.com/hostnames/{hostname}"
resp = await session.get(url)
if resp.status != 200
return False
resp_json = await resp.json()
return bool(resp_json["data"])
async def upload_payload(session, hostname):
data = f"some_payload_with_{hostname}"
resp = await session.post("https://example.com", data=data)
is_host_found = await find_host(session, hostname)
message = "host %s was found!" if is_host_found else "host %s wasn't found in time"
logger.info(message, hostname)
async def scheduler(session, base_hostname, num_uploads):
tasks = []
for i in range(num_uploads):
hostname = f"{i}.{base_hostname}"
task = upload_payload(session, hostname)
task = asyncio.ensure_future(task)
tasks.append(task)
await asyncio.wait(tasks)
await session.close()
What's happening in this piece of code?
scheduler
creates number of tasksupload_payload
equals tonum_uploads
.upload_payload
uploads a payload ans waits for the result infind_host
find_host
every 4 seconds checksf"https://example.com/hostnames/{hostname}"
endpoint and returnsTrue
if it's found. Otherwise it fails with timeout after 5 minutes.
Conclusion
tenacity
is a powerful library and if you need to retry operations in asynchronous code it's the
best choice. In other cases wait_for
will be more than enough. In my opinion it has simpler
API and doesn't force you to decorate functions.