Lesson 9 · Concurrency and stale decisions
From cognokratos/tauros-revenue · docs/course/09-concurrency.md · pinned revision facbbc927eb4
Part III: Human authority · Course map · Next: Lesson 10
Goal
Know what happens when two decisions, or a decision and a change, race, and why the outcome is always one consistent answer.
Concept
Every invoice command locks the invoice row first, so commands on one invoice run one after another. Approval also locks the destination row, so a deactivation cannot slip in halfway. A unique index allows one decision per revision, whatever the application does.
| Race | Outcome |
|---|---|
| double click / retry after timeout | one approval; both calls succeed |
| approve vs reject from two tabs | first wins; the other gets already_decided |
| agent revises while the human reads | the human's approval is stale_revision |
| destination retired while pending | approval is destination_inactive |
Code to inspect
lib/tauros/revenue/invoice/changes/decide.ex:lock/2, the replay ruleapprovals_one_decision_per_revision_indexin the approvals migration
Run it
Note
Book edition note. The "simultaneous" tests start several
Tasks, but they run inside the Ecto SQL sandbox in shared mode. All tasks use one database connection, so their queries execute one after another. The tests demonstrate the replay and conflict rules, not row-lock contention between real connections. The raw-SQL test in "Break it" is a genuine proof of the database-level guarantee (the unique index onapprovals.revision_id).
mix test test/tauros/adversarial_test.exs # describe "concurrency", "revision safety"
mix test test/tauros/adversarial_test.exs --repeat-until-failure 20
Break it
mix test test/tauros/revenue/approval_test.exs # "the database allows one decision per revision, whatever the code does"
That test inserts a second approval with raw SQL, bypassing Tauros entirely.
Why it fails
Postgres refuses it: the unique index on approvals.revision_id. Locks keep
the application consistent; the index keeps the database consistent even if
the application were wrong.
What to remember
- Lock, then check: decide against the current row.
- Retries are answered, not repeated.
- Put the last line of defence in the database.
Next: Lesson 10 · Auditability