Two faults, one root: something decides for itself what the client already knew, and decides it from state that has moved on.
A passed deadline was read as an empty basket
A booker whose hold deadline had passed, on a site whose cron had not yet run, was stuck. The seat map drew none of their places, so there was nothing to click to give one back, and every new pick was refused by a limit still counting the places they could not see. The basket page listed all of them.
Seen on a real basket: eight tickets, the deadline two hours past, cron last run before the deadline. The endpoint serving the booker's own half answered with all six seats and the pooled units; the map drew none of them.
The deadline travels in the cookie because a hold lapsing is the one change no response can announce. Both clients read it as proof that nothing was held. It says when a hold is DUE to end, not whether it HAS ended, and the sweep that ends one runs in cron, which an operator may leave hours between. Until it runs the places are still held, still counted against their booker's own limit, and still withheld from everybody else.
And a toggle decided the booker's intent for them
The place endpoint took one toggle operation and worked out whether it meant book or release from whatever it found held at that moment. The caller always knew which it meant, and threw that away.
So a booker clicking a place their map still showed as theirs, meaning to give it back, was handed a fresh booking instead: the exact opposite of what they asked, at the one moment their view was known to be out of date.
What was done
bookandreleasereplacetoggle. An unknown operation is a 400. Pre 1.0, so the old operation is gone rather than kept alongside.releasewith nothing held books nothing. It answersstate: 'gone', and the map drops every place and pooled unit it was crediting to a basket that no longer exists, then says that the place was no longer in the basket. Cause-neutral on purpose: swept by cron, emptied from the basket page or the slot page, or dropped by another tab are indistinguishable from the answer, and the message only has to explain why more disappeared than was clicked.bookon a place already held answers with its state rather than treating it as an error.- Neither client reads the deadline any more. It stays in the payload for a surface that wants to show the time remaining, which is the honest use for it.
- Trust is the digest alone, with no lifetime on top. A basket changes only when its own booker changes it, and every one of those changes travels through a request that restates the digest, so the next book or release is what corrects the view.
- The endpoint serving the booker's own half restates the stamp on its way out, so a basket that has gone clears it instead of leaving it behind until the visitor happens to mutate something. That turned out to cost a query rather than nothing, which is #3615974: The basket's held lines are read again for every caller in a request, so the visitor's own endpoint costs a query it already paid.
- The calendar stops hiding an overlay whose deadline has passed. Its own test had already recorded the contradiction: the day stayed shut because the hold was still consuming the unit, while the overlay refused to say the hold was the booker's.
Issue fork yoyaku-3615965
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 #4
mably commentedComment #6
mably commented