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.2.tar.gz (41.8 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.2-py3-none-any.whl (7.7 kB view details)

Uploaded Python 3

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

Hashes for duravoke-0.1.2.tar.gz
Algorithm Hash digest
SHA256 5466af7fc29652f0f524b2bfda8d0f1e11c08a5571b1953c05ecd6b802e5a5b4
MD5 e35870f00cfbd5ac0c18a5a1af80fb0f
BLAKE2b-256 ad745799eaa0ceb28e26bdb70f359ef2532b39665cb828462fb1cfef9101a594

See more details on using hashes here.

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

Hashes for duravoke-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 2690539f1c4b7a6d6c11b52f8cd069bc487db3c6feefd2891b06f480166083ae
MD5 6405a53cae5eec5b16a33f9e2606011b
BLAKE2b-256 72b357afdaeb3715235cce33fe24eb8a367b1a23a65a95a10057a59556c409b8

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