Cache the seat map's availability like the calendar's, and invalidate both when allotments, pinnings or the layout change

The calendar cached a resource's availability for a few seconds. The seat map did not, so it was the one availability surface still computed on every request, and the one under the most simultaneous load.

The split

The map state now splits by how long each part stays true.

Layer Holds Lifetime
Settled pool blocks, tariffs, prices until an operator edits something availability is counted from
Moving taken place ids, held place ids, pool remaining units yoyaku.settings.availability_max_age
Per request the visitor's basket, the operator's allotment colors never shared

The settled layer is keyed per session, which is what makes the prices safe to hold: a price is resolved per session, and an entry shared between them would show one evening's price on another evening's map. It is also keyed per content language, because the labels are translated.

The moving layer gets a lifetime rather than a booking tag, and the difference is the point. A rush is readers and writers on one session, so invalidating on every booking would empty the cache exactly when it is needed. A lifetime bounds the work instead: one rebuild per session per lifetime, however hard it is asked.

Nothing is risked by that. A seat map is already out of date the moment it reaches the screen, since somebody books a second later and the page sits there for minutes; the few seconds the server adds are small against that. Correctness never came from the map anyway, since a hold re-checks capacity under the slot lock.

Measured, not asserted

VenueMapCacheTest counts the statements that actually ran:

  • first visitor to a session: works the answer out
  • second visitor: 0 statements
  • next visitor after somebody books: 0 statements
  • after an operator adds a tariff: rebuilt, and the new tariff is on the map at once

Counted as statements rather than milliseconds on purpose: a timing number from a kernel test says nothing about a real request, but a statement that did not run did not run anywhere.

Two things that keep the staleness from being felt

The visitor's own basket is read live and wins over the shared answer. Without this a booker who took a seat and reloaded inside the window watched their own booking come back as an empty seat, because a place was only marked as theirs if it was in the cached taken list.

A click refused because a seat has gone makes the map ask for the whole state again, through a new GET /placement/{slot}/map-state endpoint, rather than correcting that one seat and leaving every other stale seat there to refuse the next click. That is what makes the lifetime safe to raise; yoyaku_placement.settings.resync_map_on_refusal turns it off.

The feed's tags were wrong, and had been for a while

AvailabilityController listed what availability was counted from when it was written. Allotments joined the arithmetic, then places held by name, and neither was in the list; placement was never in it at all, so closing an area or repointing a configuration left the feed stale too.

AvailabilityCacheTags collects the list instead: core declares what core counts, and any module implementing AvailabilityCacheTagsInterface declares its own, so the next feature to join cannot be left out. Bookings stay deliberately absent, with a test pinning that, because tagging them would empty every cached figure several times a second.

One lifetime

Moved to yoyaku.settings.availability_max_age (default 30), on the core booking settings form, so the calendar and the seat map cannot be tuned against each other by accident. feed_max_age is gone rather than kept as a fallback.

Verification

158/158 kernel classes pass locally. phpcs, phpstan and cspell clean on the changed files; docs (venue-map.md, calendar.md) and fr.po updated in the same commit, with a fresh potx extraction leaving nothing untranslated.

Merge request reports

Loading
Loading