inline_transaction — one transaction for the shop and the machine¶
What it does¶
A shop keeps its data in one SQLite database: its orders and stock tables, and harel’s next
to them. Placing an order is a single transaction that writes three things: the shop’s order
row, the stock the order takes, and the order machine’s state. If there isn’t enough stock, all
three roll back — no order row, no stock taken, no half-started machine. Paying updates the
shop’s row and advances the machine, again together:
place 2 widgets:
order-1: order row = placed, machine = AwaitingPayment, widgets left = 3
place 9 widgets (only 3 left):
refused: not enough widget for 9 — and nothing of it was kept
order-2: order row = —, machine = —, widgets left = 3
pay order-1:
order-1: order row = paid, machine = Paid, widgets left = 3
The machine¶
stateDiagram-v2
[*] --> Reserving
state "Reserving" as Reserving
Reserving : on enter#58; reserve_stock
state "Paid" as Paid
Paid : outcome#58; success
Reserving --> AwaitingPayment
AwaitingPayment --> Paid : Pay
Paid --> [*]
Entering Reserving runs reserve_stock, which takes the stock in the shop’s own table and
raises OutOfStock when there isn’t enough.
How it is built¶
store.py—ConnectionStore, anExecutionStorewritten outside harel. harel’s ownSqliteStoreopens its connection and commits every write itself; this one writes through the caller’s connection and never commits or rolls back — the caller’s transaction does. It is built from the public pieces: the protocol’s records and helpers fromharel.engine.store.base, the table names fromharel.engine.schema.Names, and it passes the same contracts harel’s stores do (harel.testing). See writing your own backend.run.py— the shop.open_shopcreates the shop’s tables and harel’s —store_schema("sqlite"), the schema as data (see naming and schema ownership) — and builds the runner:DurableRunner(ConnectionStore(conn), definitions, execution="inline", on_action_error="raise")
place_orderis onewith conn:block — a transaction that commits on success and rolls back on an exception — around the shop’sINSERTandrunner.create(...).actions.py—reserve_stock, which runs on the same connection.
An order refused, rolled back as one:
sequenceDiagram
participant Shop as shop (one thread, one connection)
participant R as DurableRunner (inline)
participant A as reserve_stock
participant DB as SQLite (one transaction)
Shop->>DB: INSERT INTO orders ...
Shop->>R: create("order", item, qty = 9)
R->>DB: the execution's writes (ConnectionStore)
R->>A: on enter Reserving
A->>DB: UPDATE stock ... (0 rows: not enough)
A--xR: raises OutOfStock
R--xShop: OutOfStock (on_action_error = "raise")
Shop->>DB: ROLLBACK — the order row, the stock, the execution
How it runs¶
Semantics: synchronous —
DurableRunner: the shop needs the order placed, or refused, before its transaction ends.Execution model: inline. The connection, and so the transaction, belong to the shop’s thread. On the background loop the store would run on another thread, outside the shop’s transaction; inline, every store call and every action runs right there, on the shop’s connection. A coroutine action would be refused: there is no loop to await it on.
on_action_error="raise". With the default,"fail", a raising action dead-letters the execution, and that step is committed — inside the shop’s transaction, which would then commit a failed order next to the shop’s row."raise"lets the exception reach the shop, whose transaction rolls back.
The same shape fits a sync web view whose framework keeps a connection per thread, or a batch
job that must not leave half a unit of work behind. A DistributedRunner(..., execution="inline")
does the same for queuing work: the execution and its queued Start commit with the caller’s
writes, and a worker runs it afterwards.
Run it¶
uv run python -m examples.inline_transaction.run