without-durability
Durable workflows built on the observation that once a workflow's state is a checkpoint any process can read, most of a workflow engine stops being necessary. A workflow is an ordinary async function whose effects are named; resuming means calling it again; each step it reaches hands back what is already recorded instead of running.
from without_durability import Completed, MemoryCheckpointer, Run, Sleeping, Waiting, claimed, resume
def as_text(recorded: object) -> str:
if not isinstance(recorded, str):
raise TypeError(f"{recorded!r} is not the reference this step recorded")
return recorded
async def fulfil(run: Run) -> str:
charge = await run.step("charged", lambda: gateway.charge(order), as_text)
await run.sleep("settling", timedelta(days=3)) # survives a crash on day two
approver = await run.awaiting("approved-by", as_text) # another process writes this
return await run.step("paid", lambda: gateway.pay(charge, approver), as_text)
checkpointer = MemoryCheckpointer()
match await resume(await claimed(checkpointer, "order-42"), checkpointer, fulfil):
case Completed(value=reference): ... # the workflow finished
case Sleeping(due=due): ... # schedule a wakeup for `due`
case Waiting(key=key): ... # nothing to schedule; someone must write `key`
A pass comes back as one of those three rather than raising two of them, so what to do
next is a match a type checker can tell you is incomplete. Inside the workflow a
suspension is still an exception, because that is how you stop in the middle of
straight-line code; resume is the boundary where it becomes a value.
Each read names a parser because a step hands back what the store holds, not
what its effect returned: the value has been through a codec, so a step returning
a tuple is handed a list on the pass that ran it. A parser makes the return type
something a function proved rather than something a cast asserted.
There is no server here and no engine. What there is instead is an interface, and the interface is where the interesting part lives.
A protocol of load and record is too weak to run a workflow safely at any
scale, because it cannot say "only if nobody else is running this" or "only if I
am still the one who may write". So Checkpointer states the requirements and an
implementation says how it meets them: claim grants at most one live pass and
issues strictly increasing fencing tokens, record refuses a write from a
superseded pass and never overwrites a recorded step. That is the same problem
Temporal answers with a server and DBOS answers by requiring Postgres; here it is
stated as an interface, so a deployment brings whatever store it already runs.
Scheduler is the other half of a workflow's state, its right to run, and
Durable is the pair plus the transitions that have to cross both at once.
Stores live in their own packages, so this one depends on nothing but without
and without-dag:
without-durability-redis, where each guarantee is a small Lua scriptwithout-durability-postgres, where each is an ordinary transactionwithout-durability-sqlite, the same over one file, with no server and no driver
MemoryCheckpointer and MemoryScheduler ship here too, so a test injects a dict
rather than starting a container.
The same interface carries a second mechanism: run_durably runs a
without-dag CompiledGraph against it, recording each (node key, result)
before pulling the next. A saga needs no third one, since a rollback is another
graph run through the same call. work(durable, body) turns either into a running
service, a pool of passes plus a timer, with backpressure that falls out of
pulling one delivery per free slot.
See the
without-durability guide
(with the API reference)
for the full surface: the two mechanisms, the worker, Run.transact and the
exactly-once step it buys, what each store can and cannot promise, and the gaps
that keep this a substrate rather than a replacement for Temporal or DBOS.
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 without_durability-0.0.2.tar.gz.
File metadata
- Download URL: without_durability-0.0.2.tar.gz
- Upload date:
- Size: 34.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3b3efa88591a4c468ce3da765a1eb00838042c4f9866464466008181f1bdd9b0
|
|
| MD5 |
c16664b8dcf550bb25ecd497db5a5b3e
|
|
| BLAKE2b-256 |
2fd0185bd8577155a27a6737ae85e15566a171efad97a01a72a5c0f3183edf32
|
File details
Details for the file without_durability-0.0.2-py3-none-any.whl.
File metadata
- Download URL: without_durability-0.0.2-py3-none-any.whl
- Upload date:
- Size: 40.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
311c5e022e575d3a3c1cdfec9adde25645e01606a4eda103b000d4f569ea4e71
|
|
| MD5 |
bb169001af5421fd1043a0383b7d2a2b
|
|
| BLAKE2b-256 |
d2d12fbaf319a33be3e5804f7e0351383839497f5e8559d8e21612043202d765
|