Skip to main content

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.

Run 1 (blue) Run 2 (green) Run 3 (purple)
Sent email to user_id: 0 at 1769848541 (blue) Sent email to user_id: 0 at 1769848541 (blue) Sent email to user_id: 0 at 1769848541 (blue)
Sent email to user_id: 1 at 1769848541 (blue) Sent email to user_id: 1 at 1769848541 (blue) Sent email to user_id: 1 at 1769848541 (blue)
Sent email to user_id: 2 at 1769848541 (blue) Sent email to user_id: 2 at 1769848541 (blue) Sent email to user_id: 2 at 1769848541 (blue)
Sent email to user_id: 3 at 1769848541 (blue) Sent email to user_id: 3 at 1769848541 (blue) Sent email to user_id: 3 at 1769848541 (blue)
crash 🔥 Sent email to user_id: 4 at 1769848547 (green) Sent email to user_id: 4 at 1769848547 (green)
  Sent email to user_id: 5 at 1769848547 (green) Sent email to user_id: 5 at 1769848547 (green)
  Sent email to user_id: 6 at 1769848547 (green) Sent email to user_id: 6 at 1769848547 (green)
  crash 🔥 Sent email to user_id: 7 at 1769848552 (purple)
    Sent email to user_id: 8 at 1769848552 (purple)
    Sent email to user_id: 9 at 1769848552 (purple)
    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

duravoke-0.1.3.tar.gz (43.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

duravoke-0.1.3-py3-none-any.whl (8.5 kB view details)

Uploaded Python 3

File details

Details for the file duravoke-0.1.3.tar.gz.

File metadata

  • Download URL: duravoke-0.1.3.tar.gz
  • Upload date:
  • Size: 43.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.3

File hashes

Hashes for duravoke-0.1.3.tar.gz
Algorithm Hash digest
SHA256 6531b6cf466ee6f3ebe686e07d9e04b2423d0cfd7d26b0e26f3d4b68e4ccb627
MD5 3dbfc58695bfde3cc17eddf0475934de
BLAKE2b-256 bf7e35dd69e16c4627c3ef023e3aa99d449bafc00f6dc20b8dc0a14eea3702f8

See more details on using hashes here.

File details

Details for the file duravoke-0.1.3-py3-none-any.whl.

File metadata

  • Download URL: duravoke-0.1.3-py3-none-any.whl
  • Upload date:
  • Size: 8.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.3

File hashes

Hashes for duravoke-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 e1fc8dcc382c3664b09a805f48785e7fbf69a769008905e78f558f8057e9c08a
MD5 11fe688ef206d6959b6e8d1bea452879
BLAKE2b-256 79d50ea800ad0845e12d81c829625e19e9b907b7f5626bc6f269508d1882e6cb

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page