Which seats of a session are gone is read once and shared for a few seconds (yoyaku.settings.availability_max_age, 30 by default), so a rush does not send every reader back to the database. That makes a session which has just written to a slot the one reader the shared answer is wrong for, and the seat map got it wrong in both directions.

Seats just taken were drawn as empty seats. Take seats on the slot page, then open the seat map. The seats are in the basket, the basket is read live on every request and reaches the page as mine, but mergePayload() in venue-map.js promoted a seat to the booker's own only when the shared list already called it gone. A seat just taken is in mine and not yet in taken, so it fell through to available and stayed that way until the lifetime lapsed.

The same rule is applied correctly in the resync path a few hundred lines up, where mine is consulted first and the shared list is only the fallback. The two readers of mine disagreed.

Seats just given back were drawn as somebody else's, and could not be taken back. The mirror case, and the worse one. Releasing a seat leaves nothing behind to read, so the seat stays in the shared list of gone seats. It comes back drawn as taken, and a seat drawn as taken is not clickable at all (toggle() acts only on available and mine), so the booker is refused their own seat by their own screen until the lifetime lapses, while the engine would grant it on the spot: a hold re-checks capacity under the slot lock and never consults the map's belief.

The fix

One rule covers both directions: the shared answer is corrected by what this session itself has just done, and by nothing else.

  • Taking needs no record. The basket already is the record, and mergePayload() now prefers it, which is what the resync always did.
  • Releasing gets one. ReleasedPlaceRecord notes the place ids in the private tempstore beside the basket pointer, and VenueMapBuilder::state() subtracts any younger than the lifetime from the shared list. The initial render and the resync feed both read state(), so one correction serves both.
  • ReleasedPlaceSubscriber keeps it in step from the engine's own held and released events, so a seat given back from the cart or the operator UI corrects the map too, not only one given back on the map itself.

An entry is meaningless once it outlives the lifetime it corrects, so the record prunes on every read and every write and holds no more than what one session released in the last few seconds. Nothing shared is invalidated: calling forgetMovingState() on every release is exactly the per-booking invalidation the lifetime exists to avoid, and a rush releases seats too.

Why the tests did not catch it

The fold of state onto seats exists twice, once in PHP (VenueMapBuilder::withState(), reached through build()) and once in the JavaScript the browser runs. The kernel test that pins "a booker sees their own seat even when the shared answer predates it" asserts on the PHP one. build() has no caller outside the test suite: every request builds the map from the drawing endpoint plus state() and merges in the browser. The rule was pinned on the copy nobody runs.

Guarded now by two browser tests in VenueMapPickerTest: take a seat, reload, assert it is still the booker's; then give it back, reload, assert it is free and can be taken again at once. Both widen the lifetime first, so a slow run cannot rebuild the shared list on its own and pass for the wrong reason.

Left for follow-ups

  • Pooled areas have the same defect in the same place: a pool's available comes from the shared half while its mine is live, so units taken or given back are miscounted for the lifetime. Correcting a count needs a signed delta rather than a list of ids, so it is not folded in here.
  • build() and withState() duplicate a rule that now only matters in JavaScript, and are kept alive by their own tests. Worth removing, with those kernel assertions moved onto state().

Issue fork yoyaku-3615824

Command icon 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

mably created an issue. See original summary.

  • mably committed 463981ed on 1.x
    fix: #3615824 Read a booker's own basket over the shared availability...
mably’s picture

Status: Active » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.