Move the hold clock from the booking line to the order

Every held line carries its own deadline today, so a basket does not expire, it crumbles: the line held at 10:00 lapses at 10:15 while the line held at 10:10 survives to 10:25, and the order sits in pending with fewer lines than the booker chose. The example workflow made it worse by nulling every deadline at n_suspend_holds, its first executable node, so there was no clock left to refresh.

One clock, on the order. yoyaku_transaction.hold_expires is now the only hold deadline in the system, and BookingManager::hold() is private: there is no way to hold a line outside an order.

What is in it

  • HoldPolicyResolver resolves hold_ttl, renew_hold_on_change and hold_max_lifetime channel to tenant to site, per key, so a front can override the window alone and inherit the rest. Config reads only, no query.
  • hold_started is a new timestamp, because the cap has to anchor on when this basket run began, not on created: a lapsed order is reused, and a basket reopened three days later would otherwise clamp to a past timestamp and die on the next sweep.
  • One cron query over the orders replaces the per-line sweep. A ten-line basket is one row matched and one queue item instead of ten of each.
  • SuspendHolds, WorkflowHoldOwnership, BookingResource::hold_ttl, holdDeadline(), ExpireHolds and the HOLD_EXTENDED event are deleted. Pre-1.0, so no shims.
  • The workflow slice: n_form takes until: yoyaku_hold_expires from a new yoyaku_hold_expires provider, re-checked when it fires by a yoyaku_basket_lapsed action that calls the same expireTransaction() cron calls. A payment timeout routes through a new n_unlock back to n_form instead of straight to n_release, so a booker whose basket had time left comes back to it. One expiry route in the whole workflow, ending at n_end_expired.
  • Hold fields on the tenant, channel and site settings forms, which the config would otherwise have shipped without.

Performance

Two measurement contexts, kept apart on purpose: a kernel test runs on SQLite with memory cache bins, a FunctionalJavascript test runs on MySQL with database bins. Counts from one say nothing about the other, so they are not put in the same table.

In a real request, against 0026dfa3

Same rig, same test, both sides pinned by tests that fail if the number moves.

Path before after
taking a place, the recurring click 22 20 −2
drawing the place map, warm 9 9 unchanged
calendar slot booking page 8 7 −1
cache tag lookups on the click 1 1 unchanged

The two queries off the click are the session's slot tariffs, read once before the slot row lock and once under it, now answered from the cache bin. Nothing is given back for them: those entries carry no cache tag, so no checksum read pays for them, and BookingHooks drops the one session a written slot tariff belongs to. On a site whose bins are in memory both reads leave the database altogether. See [#3615587].

The map render is unchanged because [#3615588] already answers that page's availability figure whole.

In kernel tests

Path ceiling
open a basket 10
each extra line of the same group 5
append to a basket, renewing 11
append, renewal off 9
sweep, per 2-line order 32
sweep, per 6-line order 58
sweep, 2-line order under a live workflow 52

Opening a basket was 14 before this work: the opening save now carries the window, the state, the tenant and the channel in one write instead of stamping them across two. That 14 was measured against 18435fc6, before allotments and availability caching landed, so read it as measured then rather than re-verified against today's head. The 10 is current and pinned.

Structural, and invisible in a single-request count

  • The sweep is per order, not per line. A ten-line basket is one row matched and one queue item instead of ten of each, and expires in one locked write instead of ten.
  • The sweep was superlinear and is not. Every line going terminal made the order recompute its state, so a six-line basket reconciled six times. Marking the transition for the duration took a six-line order from 72.5 queries to 55.5. SweepCostTest exists so it cannot come back.
  • Renewal writes are throttled. A renewal moving the deadline by less than a twentieth of the window is not written, which is what keeps the click off the order's write path. Turn throttle_hold_renewal off and the click is 22 again, with the order's cache tag invalidated on every place taken. A proportion rather than a number of seconds, because a tolerance negligible against fifteen minutes is two thirds of a ninety-second one. Marked experimental; removing the write entirely is [#3615509].

What is not claimed

Nothing here is a measurement of time, and nothing is measured under concurrency. Whether twenty queries at two thousand simultaneous bookers behaves like twenty times two thousand, or gives way on lock contention first, is what [#3615593] exists to answer. No capacity claim should be read out of these counts until it does.

Not in it

Order retention was drafted here and taken out: its guards are cross-module, so it needs a seam core does not have. That is [#3615441], with its own MR. payment_ttl stays unfiled and belongs to yoyaku_payment, not core.

Docs and translations

capacity-and-holds, cart, api, orchestra, payment, paying-for-a-booking, concepts and resource-type-design rewritten, and the French .po updated in the same commit.

Edited by Frank Mably

Merge request reports

Loading
Loading