Types and data ยท 12 min
Things happening at once
Most bugs here only appear when two things overlap. So the design leans on the database, where the rules hold no matter how requests interleave.
You can't reason about this by reading one function, because the bug lives between two of them. So step through these frame by frame, and watch the middle column.
Frame by framePick a race and step through it
Account A's check
The database
Account A's claimpending
Account B's claimpending
Owner index
EmptyAccount B's check
Both accounts have a pending claim on example.com, and both have published a valid record. Their checks finish at the same moment.
Step 1 of 6
Predict it first
Two accounts tie. A wins, and B stays pending. B's record is still valid. What happens on B's next check?
Three tools, three jobs
| Tool | Protects against |
|---|---|
| Row lock | Two writers inside the database at the same moment |
| Version and expectedVersion | A decision based on something read earlier, like a stale screen |
| Unique index | Breaking the rule itself, whatever the code does |
| Lease | Another worker starting the same slow job, after the locks have gone |
How this is actually tested
These run on real Postgres, with up to 12 connections, because the behaviour only exists when separate transactions overlap.
| Test | Why it's convincing |
|---|---|
| Two accounts verify, 25 rounds | Asserts exactly one owner, every round |
| Second writer blocks, then loses | Deterministic: holds a transaction open and confirms the other hasn't finished |
| First rolls back, waiter wins | The other branch of the same mechanism |
| A locked row is skipped | Without SKIP LOCKED, it hangs and times out. This was tried |
| 8 workers over 40 claims | None leased twice, all leased |
| 10 transitions at once | The count ends at exactly 10 |
| One token consumed by 8 callers | Exactly one gets it |