perf(scheduling): the availability grid answers 304 when nothing moved, with the poll cost measured - #1334
perf(scheduling): the availability grid answers 304 when nothing moved, with the poll cost measured#1334teetangh wants to merge 6 commits into
Conversation
ADR 16 put slot freshness on a 60s poll, so this endpoint is re-asked once a minute by every open calendar — but nobody had ever measured it. EXPLAIN (ANALYZE, BUFFERS) and Prisma's query log against the shared instance: a public poll is 8 statements / ~140 ms, a detail poll 18 / ~205 ms, and the occupancy query is 15 LEFT JOINs whose planning (8.9 ms) costs more than its execution (7.6 ms) and does not depend on how many rows come back. No index was missing; the remaining seq scans are correct on tables of a few hundred rows. Records the poll-per-minute arithmetic the conditional GET is judged against, and why the ConsultantBusyInterval read model stays deferred (#874). Part of #1319 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgadJydWEKkdhhzY58yiL7
The measured cost of a poll is 8 statements (public) or 18 (detail), paid whether or not anything changed. A change marker now decides that in ONE indexed statement (~32 ms, the round-trip floor) before any of that work runs: profile updatedAt, newest availability row, newest booked slot among the appointments reaching this consultant, newest parent request row, and the earliest STILL-FUTURE pending payment deadline — the clock fold, so a hold lapsing changes the tag even though no row was written. Raw SQL against the ORM-first rule on purpose: PG_POOL_MAX=1 serialises Prisma reads and Promise.all buys nothing (#1117), so ten aggregates would be ten round trips — slower than the query the marker exists to skip. Computed AFTER the authorization gates, so a caller who lost access is refused there and a 304 can never serve stale permission. Conservative by construction: consultant-scoped, not window-scoped, so it over-invalidates rather than risk a stale 304. ADR 16, #997. Part of #1319 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgadJydWEKkdhhzY58yiL7
The calendar keeps the last response's ETag and echoes it as If-None-Match, so an unchanged 60s poll costs the server one indexed read and this hook an early return — no setState, so no repaint of every cell for an answer that did not change. The tag is stamped only after the body validates, or a rejected payload would 304 the next poll into keeping it. Skipped on the post-allocation refetch: that caller just mutated and wants the body. Sending a conditional header makes fetch treat the request as no-store (Fetch spec 4.6), so the browser's own 30s freshness shortcut no longer short-circuits it — the trade buys a client that never repaints from a body the browser may have evicted. Part of #1319 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgadJydWEKkdhhzY58yiL7
Core logic over breadth: the route must skip BOTH heavy reads on a match (asserted, not implied), issue a new tag when a slot row moves, and issue a new tag when only the clock fold moves — the hold-lapse case that has no row write behind it. Part of #1319 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgadJydWEKkdhhzY58yiL7
Part of #1319 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgadJydWEKkdhhzY58yiL7
Part of #1319 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgadJydWEKkdhhzY58yiL7
✅ Deploy Preview for familiarise ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Warning Review limit reachedNext included review available in 37 minutes. View limit detailsLimit details: You’ve used the included review currently available. Your 60 included PR review attempts over the past 7 days set your current allowance at 1 review per hour. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (8)
Comment |
|




What
Availability grid conditional GET + measurement (wave-5 PR 9 of #1319, narrowed from the original read-model plan).
docs/booking/20-availability-grid-cost.mdrecords what one poll ofGET /api/slots/availability-with-allocation/[consultantId]costs on the real database: the public path runs 8 statements in about 140 ms, the detail path 18 in about 205 ms; the occupancy query's cost is planning-dominated and independent of result size; every consultant-scoped arm already hits an index, so no index was added.updatedAt, the max availabilityupdatedAt, the max slot and parent-requestupdatedAtover the consultant's reach, and the earliest upcomingPayment.expiresAtof PENDING holds (so the ETag moves when a hold lapses by the clock).If-None-Matchequal →304with the sameCache-Control, skipping the occupancy queries. Over-invalidation by design (consultant-scoped, not window-scoped); authorization stays outside the marker.useCalendarDatasendsIf-None-Matchand treats 304 as unchanged.Cache-Controlstaysprivate, max-age=30(The slot grid never refreshes: staleness is unbounded until a 409 at submit #1164 dropped SWR on purpose); one shared constant for 200 and 304.ConsultantBusyIntervalread-model table waits until the load gate (Capacity estimation & load-test go/no-go: concurrent→DAU→MAU model, binding ceilings, vendor-tier blow-ups #874) shows the query, not the payload, is the bottleneck.One claim in the measurement doc was checked and does not hold on
dev: both occupancy selects already carrypaymentStatusalongsideexpiresAt, so the status-aware dead-hold rule has its inputs; the doc's sentence about an inert expired-hold check is removed in a follow-up commit if it survived the merge.Verification
Cold
tscclean after mergingdev; eslint zero warnings; schedule + booking-algorithm + payments suites green. One pin: 304 on an unchanged marker, 200 on a changed one.Part of #1319. Part of #997 (grid perf).
🤖 Generated with Claude Code
https://claude.ai/code/session_01MgadJydWEKkdhhzY58yiL7