perf(workshops): cache show pages and their sanitized renders - #2954
Merged
Merged
Conversation
mroderick
force-pushed
the
feature/workshop-show-caching
branch
from
September 28, 2026 06:37
088a251 to
0ec3365
Compare
WorkshopsController#show was the app's top allocation consumer (28% of allocations, p50 ~7.3k objects per render) because the page had no caching at all. - Conditional GET (fresh_when) for anonymous show requests, keyed on the workshop, its section-bearing records (host/address, sponsors, organisers), locale, and a template bump token; repeat and crawler visits get 304s without rendering. Logged-in requests are untouched because the actions section is per-user. - Fragment-cache the description (sanitized inside the fragment), the venue, sponsors, organisers grid, and newsletter sections, keyed on the workshop and the records that mutate independently of it, so description/sponsor/venue/organiser changes invalidate correctly. The description stays raw HTML in the database and is sanitized on cache misses only — no write-path invariant, no backfill migration, and future bulk writers keep render-side protection. Benchmarked against a codebar_production_dump clone with the top-10 requested workshop URLs from production logs: anonymous repeat visits 14.6k -> 7.4k allocations (-49%, 304 path), logged-in renders skip the heaviest sections. Review run 20260927-232127-ad7e4667: findings #1, #2, #4 addressed.
mroderick
force-pushed
the
feature/workshop-show-caching
branch
from
September 28, 2026 06:49
0ec3365 to
5448da8
Compare
mroderick
marked this pull request as ready for review
September 28, 2026 07:00
KimberleyCook
approved these changes
Sep 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Workshop show pages were the app's top allocation consumer — 28% of all allocations (p50 ~7,348 allocations and 8 queries per render) — because the page had no caching at all. They now ship with: conditional GET (304s) for anonymous visitors, fragment caching of the static sections, and the description render cached with sanitization inside the fragment (the issue's "cache the sanitized HTML" variant — the description stays raw HTML in the database, so there is no write-path invariant and no backfill migration).
Measured against the production data: top-10 most-requested workshop show URLs from 3 days of canonical logs (including one virtual workshop), served from a
codebar_production_dumpclone, base commit vs this branch,GC.stat(:total_allocated_objects)medians of 5 runs per URL via a throwaway probe (not committed). Logged-in figures include the fragment-hit path; the 304 range is the etag computation itself (workshop + host/address + sponsors + organisers queries), which is what keeps conditional GETs honest after a sponsor or organiser change. Absolute numbers differ from production; relative profile transfers.Fixes #2951.
Design decisions
fresh_whenruns behind!logged_in?and logged-in members always get a full render. The ETag mirrors the fragment keys (workshop + host/address + sponsors + organisers + locale + bump token), because those records mutate without touchingworkshops.updated_at— without them a sponsor or organiser change would leave repeat anonymous visitors on a stale 304.sanitize(@workshop.description)runs on cache miss only, keyed on the workshop — the issue's "cache the sanitized HTML" variant rather than sanitize-on-write. Storage keeps the raw authoring HTML (nothing is irreversibly rewritten, no data migration), the sanitizer stays at the render boundary so bulk writers bypassing Rails remain safe, and a future allowlist widening re-renders old descriptions correctly.fresh_whenrenders a 304 without halting, and the virtual-workshop render after it raisedDoubleRenderError;return if performed?fixes a latent 500 the new virtual-path spec exposed.Testing
<script>+ bold) rendered sanitized with the raw source preserved.Post-Deploy Monitoring & Validation
payload.allocationsp50 forWorkshopsController#showon the Codebar requests dashboard should drop materially;sum by (controller) (count_over_time({app="planner", controller="WorkshopsController"} | json | payload_status >= 500 [5m]))for 5xx regressions.curl -s -o /dev/null -D - https://www.codebar.io/workshops/<known-id>captures the ETag; repeat withIf-None-Matchmust return 304. Repeat after a description edit to confirm the ETag rotates (200 with new content).Unapplied review findings
From code-review run
20260927-232127-ad7e4667(verdict: Ready with fixes):