The last-operator guard ran on the pool, before the transaction opened. Two
hosts deleting themselves at the same moment each saw the other, both passed,
and the event was left with nobody who can moderate, nobody who can release
the gallery, and no way to appoint anyone — because appointing requires a
host. Not recoverable from inside the app.
The fix is a transaction-scoped ADVISORY lock, and the two obvious
alternatives are both worse:
* `FOR UPDATE` on the other operators' rows DEADLOCKS. Each deleter locks the
other's row and then tries to delete its own, so Postgres resolves it by
killing one. The invariant survives; the loser gets a 500 instead of the
sentence explaining what to do. My first attempt did exactly this, and the
test caught it.
* Locking the `event` row serialises cleanly but inverts the lock order every
moderation path takes (upload/user rows first, event last). That is an ABBA
against a path that runs constantly during the event, traded for one that
runs approximately never.
An advisory lock is a separate lock space, so it cannot interact with the
row-lock graph at all, and it is released when the transaction ends. The loser
waits, counts zero once the winner's row is gone, and is refused with the
sentence it should have got.
The test is a genuine concurrency test — it spawns the second deleter and
asserts it unblocks to see no remaining operator. It fails against the
pre-check-outside-the-transaction version and against the FOR UPDATE version.