Stores¶
A store (ExecutionStore) is how a runner persists and reloads an Execution. This is the
hub for the per-backend reference: the contract every backend implements, the durability shape
they share, the opt-in execution trace — and a page per backend with its exact data model and
every operation (what each table/key/document holds, how each update is done, and why). For the
concepts (the seam, surviving a restart, idempotency) start with durability; to
select a store at the worker see STM_STORE_BACKEND.
The contract every backend implements¶
ExecutionStore is a small Protocol (store/_base.py) —
each backend is a sibling module under harel/engine/store/, re-exported from the package, with
a twin under harel/engine/aio_store/ (same data model, awaited IO):
Method |
Purpose |
|---|---|
|
rehydrate the Execution (the latter folds the dedupe check into the same round-trip) |
|
persist with the version CAS (raises |
|
the one atomic write per event — the Execution + outbox + dedupe + timer ops + spawn intents + (opt-in) trace step, all-or-nothing |
|
dedupe lookup |
|
the relay drains emitted events |
|
the relay drains orthogonal child-creation intents |
|
the timer sweep |
|
the opt-in execution timeline (see below) |
|
a page of lightweight summaries for the monitor |
|
release the connection/client |
Everything commit writes lives in one transaction (or one atomic request), which is the
property that makes a crash safe — see durability. The common shape across the
durable backends, which each per-backend page then spells out exactly:
CAS — a
versionper record; the write applies only if the stored version still matches, elseStoreConflict. This is the single-writer-per-execution backstop.Outbox / spawns — emitted events and region-spawn intents are persisted with the advance and delivered by the relay afterwards, so a crash never loses a
Finishedor a fork.Dedupe —
processed_events(id + event id) makes at-least-once delivery effect-once.Timers —
(execution_id, path) → fire_at, armed/cancelled in the same commit.
Execution trace (opt-in)¶
When tracing is on (STM_TRACE=1, or DurableRunner(..., trace=True)), commit also appends
one timeline step — event in, transition from → to, the actions that ran, and the
resulting context_out — in the same transaction as the advance. It is off by default, so
the hot path pays nothing; when on it costs ~one extra in-transaction write (~+10 µs/commit on a
local SQLite, no extra round-trip or fsync), and load is unaffected (it still reads the
snapshot — there is no event replay). Each backend keeps a ring of the last STM_TRACE_MAX
steps (default 200) using its natural primitive (a capped table, an LTRIM’d list, a
$slice’d array, a Put+Delete window); the monitor renders it as the timeline. Only
context_out is stored (the monitor derives each step’s context_in from the previous step).
from harel import definition_from_dsl, DurableRunner, SqliteStore, Event
defn = definition_from_dsl(
"event Go {} machine m { initial A state A {} final Done success {} from A to Done on Go }", "m"
)
store = SqliteStore(":memory:")
runner = DurableRunner(store, {defn.id: defn}, trace=True) # opt-in
exe = runner.create(defn.id)
runner.process(exe.id, Event(kind="Go"))
for step in store.read_trace(exe.id):
print(step["index"], step["event_kind"], step["from_path"], "->", step["to_path"])
0 Start None -> A
1 Go A -> Done
The backends¶
Each backend has its own page with the full schema/key-space and every operation broken down with the real SQL/commands and the reasoning:
- DictStore — in-memory (default)
- SqliteStore — one machine, zero infrastructure
- LibsqlStore — libSQL / Turso (experimental)
- RedisStore — all-network, no shared filesystem
- PostgresStore — distributed SQL
- RqliteStore — distributed SQLite over Raft
- MongoStore — document store
- DynamoDBStore — AWS serverless
Backend |
For |
CAS mechanism |
|---|---|---|
tests, embedding (in-memory, not durable) |
object identity |
|
one machine / shared volume, zero infra |
|
|
libSQL/Turso — file, |
|
|
all-network, pairs with |
atomic Lua CAS (state-only fast path); |
|
distributed SQL |
|
|
HA SQLite over Raft (HTTP) |
guarded upsert, one request |
|
document store, single-document atomic |
|
|
AWS serverless, pairs with |
conditional write + |
Async ports¶
Every store has an Async… twin under harel/engine/aio_store/ with the same data model —
the async worker (STM_CONCURRENCY events in flight on one loop) talks to those. Most are native
async (aiosqlite, redis.asyncio, psycopg async pool, motor, aioboto3, httpx for rqlite);
AsyncLibsqlStore wraps the sync driver on a thread (the libsql package is sync-only). The
synchronous Store classes are what the embedded DurableRunner/DistributedRunner façades and
the monitor use, bridged to the async core through the anyio portal. Each per-backend page has an
Async twin section with the specifics.