A mini durable execution library, with an extreme focus on simplicity
Project description
Duravoke
A (experimental) mini durable execution library, with an extreme focus on simplicity.
Install
via pip: pip install duravoke
via uv: uv add duravoke
What is Durable Execution?
While there are already great answers to this question, the simplest definition is this:
Durable execution means your code can crash, restart, and still finish exactly once.
As long as you wrap your critical methods with duravoke, you can rest assured that you can keep calling them until they eventually succeed, after which subsequent calls will be idempotent.
Design Principles
While durable execution is a core feature of many workflow execution frameworks (e.g. Temporal, LangGraph, Inngest, etc.), they also come with a lot of extra baggage: specific database requirements, multiple required microservices, paywall gated features, and steep learning curves.
That baggage, while in many ways is necessary for mature software, is overkill in a world being infiltrated with vibe-coded micro SaaS applications. This type of software needs something simpler to setup and easy to understand.
Demo
Here is a toy snippet to set the stage of a codebase that needs to run some very error prone process.
In this case, we need to send emails to 10 users, but our send_email method is very flaky. It has a 50% probability of just crashing our server entirely 🔥
import asyncio
import time
import random
async def send_email(user_id: int):
if random.random() > 0.5:
print("crash 🔥")
quit()
curr_time = round(time.time())
return f"Sent email to user_id: {user_id} at {curr_time}"
async def send_emails(user_ids: list[int]):
for user_id in user_ids:
email_output = await send_email(user_id)
print(email_output)
print("finished 😎")
asyncio.run(send_emails(list(range(10))))
Math tells us that the above code has a ~0.1% chance of ever printing "finished 😎".
In other words, you would need to run it 1024 times to ever have a chance of seeing it print "finished 😎".
Now, just add the @duravoke.duravoke decorator to both methods.
# hello_durable_flaky.py
import asyncio
import time
import random
from duravoke import Duravoke, PersistedKKV, JSONSerializer
kv = PersistedKKV("./duravoke_state.json")
duravoke = Duravoke(kv, JSONSerializer())
@duravoke.duravoke
async def send_email(user_id: int):
if random.random() > 0.5:
# Our Email API has a 50% chance of failing :(
print("crash 🔥")
quit()
curr_time = round(time.time())
return f"Sent email to user_id: {user_id} at {curr_time}"
@duravoke.duravoke
async def email_users(user_ids: list[int]):
for user_id in user_ids:
email_output = await send_email(user_id)
print(email_output)
print("finished 😎")
asyncio.run(email_users(list(range(10))))
Now, while the above code still has a high chance of failure, subsequent reruns of it will pick up from where it left off.
The below logs show an example of running python hello_durable_flaky.py 3 times.
| Sent email to user_id: 0 at |
Sent email to user_id: 0 at |
Sent email to user_id: 0 at |
| Sent email to user_id: 1 at |
Sent email to user_id: 1 at |
Sent email to user_id: 1 at |
| Sent email to user_id: 2 at |
Sent email to user_id: 2 at |
Sent email to user_id: 2 at |
| Sent email to user_id: 3 at |
Sent email to user_id: 3 at |
Sent email to user_id: 3 at |
| crash 🔥 | Sent email to user_id: 4 at |
Sent email to user_id: 4 at |
| Sent email to user_id: 5 at |
Sent email to user_id: 5 at |
|
| Sent email to user_id: 6 at |
Sent email to user_id: 6 at |
|
| crash 🔥 | Sent email to user_id: 7 at |
|
| Sent email to user_id: 8 at |
||
| Sent email to user_id: 9 at |
||
| finished 😎 |
Note how when a email was sent to a user in one run, it logs a timestamp. In subsequent runs, that same timestamp is logged. The user isn't sent a duplicate email.
Each run of hello_durable_flaky.py becomes idempotent based on the last run. Moreover, this idempotency is completely abstracted away for you the developer. All you need to do is add the @duravoke.duravoke decorators.
How do I use this IRL?
Ok great, now you can feasibly finish sending all 10 users an email without having to worry about:
- Running the script 1024 times (durable execution)
- Sending duplicate emails (idempotency)
But you still need a way to call the email_users method until it finishes the entire user list. And that is the part that duravoke is unopinionated on. You can just keep a list of tasks to execute in a database or a queue, and a cron job for reading those tasks, and calling your @durovoke.duravoke decorated method with the task's parameters.
There are great libraries for managing tasks queues, such as celery or bullmq.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file duravoke-0.1.2.tar.gz.
File metadata
- Download URL: duravoke-0.1.2.tar.gz
- Upload date:
- Size: 41.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5466af7fc29652f0f524b2bfda8d0f1e11c08a5571b1953c05ecd6b802e5a5b4
|
|
| MD5 |
e35870f00cfbd5ac0c18a5a1af80fb0f
|
|
| BLAKE2b-256 |
ad745799eaa0ceb28e26bdb70f359ef2532b39665cb828462fb1cfef9101a594
|
File details
Details for the file duravoke-0.1.2-py3-none-any.whl.
File metadata
- Download URL: duravoke-0.1.2-py3-none-any.whl
- Upload date:
- Size: 7.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2690539f1c4b7a6d6c11b52f8cd069bc487db3c6feefd2891b06f480166083ae
|
|
| MD5 |
6405a53cae5eec5b16a33f9e2606011b
|
|
| BLAKE2b-256 |
72b357afdaeb3715235cce33fe24eb8a367b1a23a65a95a10057a59556c409b8
|