Issue #3618572: Stop a claim counting the hall again, and say what memcache cache tag checksums buy
Two halves of #3618572: what to set on a site today, and the recount that was making a claim cost a fresh count of the house.
The documentation half
docs/sizing-for-a-rush.md told the reader that cache tag checksums on memcache were unproven and the default should be kept. Measured properly, with the arms alternated and the {cachetags} counter read around each run to prove which arm ran, they are worth having: the median hold falls from 244-249ms to 154-160ms and the ninety-fifth percentile from about 795ms to about 283ms. Throughput moves too, between ten and sixty per cent depending which rounds are paired, because the database arm itself swings from 184 to 256 holds a second on this machine, and the page's own warning about drift applies to its figures as much as anybody's.
The mechanism is stated correctly now as well. Core writes that row after the transaction commits rather than inside it, so no hold keeps it while it works: what forms is a convoy of short single row updates, one per request that books, and every booker on the site shares the row.
The cost is said plainly too: memcache stops being optional, because tag invalidation becomes its job.
The recount half
Building the offer card asks how many seats each block has left. That figure is kept for availability_max_age and adjusted by whatever the request has taken since, which is right, but two escape hatches abandoned the adjustment and left the next reader to count the hall: a seat whose block was not in a read already made, and any seat given back. On the stadium at four bookers, 426 and 532 of those for 382 holds, ending in 132 fresh counts. Each count examines about 160,000 rows to produce eighty numbers.
Neither hatch was necessary, because in every case the block was already known to somebody:
- The hold path is handed the seat itself.
tallyPlace()takes aPlaceInterfacerather than an id, and the caller has it loaded already, sinceprimeForLines()loads every place the party named. No lookup at all. - A give-back is raised from an event, so the seat is not in the caller's hands, but it is almost always in a read this request has made: the seating reads the block it is rearranging within.
giveBackPlace()asks those reads first, and only a seat nothing has touched costs one entity load, which is a row by primary key against a count of the hall.
countsUnadjustable and forgetAreaCounts() have no callers left and are gone. A line holding units of a pooled area moves none of these counts, which is also correct: what a pool has taken lives on its anchor row.
Measured, on the claim journey against itself
Forty bookers on a house of 80,000 seats in 80 blocks, each naming its own block, on an idle machine, both arms on the shipped database cache tag checksums (the live class was printed for each run so the backend is on the record):
| holds/s | statements per hold | rows read per hold | median | |
|---|---|---|---|---|
| 1.x | 125 and 163 | 55.3 to 56.6 | 33,572 and 32,354 | 285ms and 194ms |
| this branch | 140 and 155 | 56.1 to 56.4 | 13,878 and 15,026 | 206ms and 201ms |
Rows read per hold fall by 56 per cent. That is the claim this makes, and it is work rather than time, so the rig's noise cannot flatter it.
Throughput is not distinguishable on this machine, and an earlier version of this description said otherwise. The arms overlap, 125 to 163 against 140 to 155, and the figure that suggested an improvement was a single earlier reading taken while the box had other work on it. A single container with fourteen cores, the database local and everything warm is not where 19,000 fewer rows per hold shows up as throughput; a database under real pressure is.
The journey is the heaviest one there is: a booker keeps their session and presses the stepper eight times, so every press re-seats a party one seat larger. One hold counted is one seat added, not one booker served.
Two things tried and deliberately not here. A stampede guard, so that one reader refreshes an expired figure while the others use it: the default lock backend is the database, so it answered a shared row with another shared row and throughput fell. And resolving the block eagerly on every seat taken: that charged every click on a seat map for a figure the map never asks for, which PlaceMapPerformanceTest caught immediately and correctly.
Tests
AreaCountsKeptTest covers the adjustment in both directions and now passes the seat rather than its id, which is the API change made visible. PlaceHoldCostTest, PlaceReleaseCostTest, MapLayoutCostTest and VenueMapEndpointCostTest pass with their query budgets unchanged, which is the point: the map journey pays nothing for this. SeatTogetherTest, OrphanedPlacesTest and AreaCounterTest pass, and they are the ones that would fail if an adjusted count were wrong.
phpcs with the CI standard, cspell and the translation check are clean.
Narrowing the tag: considered and dropped
An earlier version of this branch, and of the issue, held that narrowing what a booking invalidates was the remaining work. It is not, for two reasons.
The first is that it buys nothing where it counts. A rush is a rush on one session, so a per-session tag is written by every booker of that session anyway and the queue is unchanged. It would relieve contention only between sessions that are not the busy one.
The second is what it costs. Three surfaces depend on the global tag: the shipped views.view.yoyaku_bookings, AllotmentAccessControlHandler, and the bookings form in yoyaku_ui. One of those is an access check on whether an allotment may be deleted, where a stale answer is worse than a slow one.
An availability argument for narrowing also appeared in the issue and was wrong: AvailabilityCacheTags::CORE_TYPES lists slots, tariffs and allotments and deliberately no bookings, so availability was never invalidated by a booking. The issue is corrected.
Also here: the driver
tests/load/claim.php. Every figure above comes from the claim endpoint, and no driver for it was in the repository: the numbers were unreproducible by anyone but the machine they were taken on. It walks either shape, --mode=step for a stepper pressed repeatedly and --mode=once for a quantity chosen in one go, and --blocks spreads the bookers over the house instead of queueing them all in whichever block the seating fills first.
Worth knowing from it, at eight bookers on the stadium: stepping to a party of eight runs at 37.8 holds a second, and asking for a party of three in one claim at 150.9. The stepper is the heavy shape, and it is the one every measurement on this issue used.