BEGIN IMMEDIATE; on Postgres it is INSERT … ON CONFLICT (effect_key) DO NOTHING with the row count checked, under the default isolation level. Every later transition is
a conditional UPDATE that matches on the state it expects, with the row count checked again. The
guarantee is the unique index, not the isolation level.
The two rows that must never be merged
This is the part that decides whether a lost database connection costs you a refund or a duplicate one.
Before
COMMIT: nothing committed, retry the write. During COMMIT: unknown, so re-read.
Two rows, two behaviours, two tests. Reading the first as the second refuses work a single query
could have recovered. Reading the second as the first retries a write that already landed.
A serialization failure sits in the first row on purpose: the server is telling you in band that
it rolled the transaction back, which is the closest a database gets to your executor raising
NotExecuted. A cancelled statement is not in that set, because a statement timeout during a
COMMIT is exactly the unknown case.
What the re-read looks for
It is an identity check on the whole row, never a match on the action id. Two attempts can share an id: it names an attempt rather than a process, and it is caller-supplyable — a caller that rebuilds an action with the id it saved has handed the store two attempts wearing one name. So “a record carrying our id” is satisfied by another process’s live reservation, and concluding you hold the key there is a double execution.- Finds the row you were about to write, column for column: the commit landed, proceed.
- Finds nothing: it did not land, retry the insert once.
- Finds anything else: hand it back to the same planner every reservation uses, and obey it.
UPDATE once. Refusing there would turn a committed effect into a human’s problem, and a proven
failure into an unknown one.
What this does not do
- It does not tell you what the remote did. Reservation is about the store. An executor that
raised something other than
NotExecutedleaves anAMBIGUOUSeffect that no query settles. - It does not reclaim a key. A lease that lapsed becomes
AMBIGUOUSwhen the next contender arrives. Nothing sweeps. - A failed re-read costs availability. The key stays reserved by an attempt that will never run, its lease lapses, and a person answers. That is the trade: it never costs a second execution.
- It does not hold a row lock across your executor. Advisory locks,
SERIALIZABLEandSELECT … FOR UPDATEwere each rejected, and the reasons are in the specification.
T155 — the connection is killed during COMMIT on purpose rather than raced
for — T155b for a landed commit seen as landed and T155e for the update the store re-issues
when it was not, T155c for the identity check that is not a match on the action id, T156 for
the failed re-read that writes nothing, and T157
and T158 for one winner across two hosts and a kill -9 mid-transaction. G4 in ctrlrun verify contends real OS processes on your store’s backend, in a scratch store of its own.
Next
- Recovery after a crash: what a restarted process may conclude.
- Outcomes and AMBIGUOUS: the other unknown, the one no query settles.
- Get started · Why.