Skip to content

perf(scheduling): the availability grid answers 304 when nothing moved, with the poll cost measured - #1334

Open
teetangh wants to merge 6 commits into
devfrom
perf/availability-grid-conditional-get
Open

perf(scheduling): the availability grid answers 304 when nothing moved, with the poll cost measured#1334
teetangh wants to merge 6 commits into
devfrom
perf/availability-grid-conditional-get

Conversation

@teetangh

@teetangh teetangh commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

What

Availability grid conditional GET + measurement (wave-5 PR 9 of #1319, narrowed from the original read-model plan).

  • Measured first. docs/booking/20-availability-grid-cost.md records what one poll of GET /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.
  • Conditional GET. One indexed marker read (~32 ms) computes a strong ETag from the profile's updatedAt, the max availability updatedAt, the max slot and parent-request updatedAt over the consultant's reach, and the earliest upcoming Payment.expiresAt of PENDING holds (so the ETag moves when a hold lapses by the clock). If-None-Match equal → 304 with the same Cache-Control, skipping the occupancy queries. Over-invalidation by design (consultant-scoped, not window-scoped); authorization stays outside the marker.
  • Client. useCalendarData sends If-None-Match and treats 304 as unchanged. Cache-Control stays private, 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.
  • Deferred with the numbers attached: the ConsultantBusyInterval read-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 carry paymentStatus alongside expiresAt, 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 tsc clean after merging dev; 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

teetangh and others added 6 commits September 3, 2026 01:28
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
@teetangh teetangh added performance Performance improvements and optimizations launch: pre-mvp Gates launch — money, data, or a failure we would not detect booking Booking, scheduling, slots, reschedule, cancellation labels Sep 2, 2026
@netlify

netlify Bot commented Sep 2, 2026

Copy link
Copy Markdown

Deploy Preview for familiarise ready!

Name Link
🔨 Latest commit 401fb11
🔍 Latest deploy log https://app.netlify.com/projects/familiarise/deploys/6a98866b4d15000008f00b8d
😎 Deploy Preview https://deploy-preview-1334--familiarise.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 36 (🔴 down 22 from production)
Accessibility: 90 (no change from production)
Best Practices: 83 (no change from production)
SEO: 90 (no change from production)
PWA: -
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 37 minutes.

Check out review usage here.

View limit details

Limit 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.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 1e7515b1-6256-483d-a964-6bb45c289edc

📥 Commits

Reviewing files that changed from the base of the PR and between a720736 and 401fb11.

📒 Files selected for processing (8)
  • __tests__/schedule/availability-grid-conditional-get.test.ts
  • app/api/slots/availability-with-allocation/[consultantId]/route.ts
  • docs/booking/05-troubleshooting-and-changelog.md
  • docs/booking/20-availability-grid-cost.md
  • docs/booking/README.md
  • hooks/scheduling/useCalendarData.ts
  • lib/scheduling/allocationService.ts
  • lib/scheduling/availabilityGridMarker.ts

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

sonarqubecloud Bot commented Sep 2, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

booking Booking, scheduling, slots, reschedule, cancellation launch: pre-mvp Gates launch — money, data, or a failure we would not detect performance Performance improvements and optimizations

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant