A seat is booked at most once per slot, and what guarantees it today is the slot's own seat rows: the rule asks whether the seat is free, the seat table answers, and the hold proceeds. The bookings themselves carry no key that would refuse a second one. So the guarantee is only as good as the maintenance of a derived table, and derived data drifts: the hooks that keep it true fire on the booking entity precisely because that is the one place every writer is caught, which is an argument that other writers exist.
The write that claims a seat already refuses to lose a race. It moves only rows that are still free and compares how many it moved against how many it claimed, so a seat that went between the check and the write takes the whole hold back rather than committing a booking on top of somebody else's. That closes one direction. The other stays open: a table that says a seat is free when a consuming booking already holds it lets the claim succeed, and nothing downstream notices.
The proposal is to put the invariant where it cannot drift. A column on the booking holding the slot and the place it consumes, NULL whenever the line does not consume, with a unique key over it. Repeated NULLs do not collide, so released, cancelled and expired lines stop competing for their old seat, while two consuming lines on one seat become impossible for the database rather than unlikely for the code. It is the value seatHeldBy() already computes, written down.
Two things it has to get right. A line given up and replaced within one hold must release before its replacement is written, which is the order the manager already uses. And a violation has to reach the booker as a refusal rather than as a fatal error, which means catching the integrity exception where a capacity refusal is raised today.
It belongs on its own issue rather than on #3616896: Seat a party under the hold's own locks, so a rush is served rather than refused, which introduces the seat rows: this changes the booking entity's schema and its hottest write path, and it wants its own measurement of what a hold costs before and after.
AI-Generated: Yes (Claude Code was used to help draft this issue summary. I reviewed it before posting; there is no code on this issue yet.)
Issue fork yoyaku-3617494
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
mably commentedDone on #3616896: Seat a party under the hold's own locks, so a rush is served rather than refused, which merged. A booking carries the seat it consumes, as the slot and the place together, with a unique key over it: the column is empty whenever the line consumes nothing or names no seat, so released, cancelled and expired lines and pooled units are outside the rule rather than competing for their old seat. A violation reaches the booker as a refusal rather than a fatal, raised where a capacity refusal is, and the presave refuses a writer that skipped the hold before the key is ever reached.
The test makes the slot's seat rows lie, by clearing the flag a hold set, and asserts the refusal did NOT come from the guard that reads them: a test accepting either refusal would pass with no key at all. Without the key it fails with the second booking written. Measured on a hall of eighty thousand places under forty bookers at once, the key costs nothing: 54.3 seats a second with it against 53.9 without, run as A, B, A.
AI-Generated: Yes (Claude Code wrote the implementation, its tests and the measurement, and drafted this comment. I reviewed it before posting.)