From 7b57f495722a766fa9d0869a376d3811630a9074 Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Thu, 17 Sep 2026 11:05:50 +0200 Subject: [PATCH 1/2] Cache the rendered /sponsors page body Cache the rendered page in the shared cache store, keyed on "sponsors/index/v1/#{Sponsor.active.maximum(:updated_at)&.to_fs(:usec)}", so a warm request costs one cache read instead of re-rendering 721 sponsor fragments (~1.3s CPU-bound per render). Solid Cache is DB-backed, so the body also survives deploys and dyno restarts. The key is computed before the records load so a save committing between the two queries self-heals on the next request, and the v1 segment lets a reviewer invalidate the body when the view changes. --- app/controllers/sponsors_controller.rb | 6 ++++ spec/requests/sponsors_spec.rb | 43 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 spec/requests/sponsors_spec.rb diff --git a/app/controllers/sponsors_controller.rb b/app/controllers/sponsors_controller.rb index 40cd41b1c..64db3a955 100644 --- a/app/controllers/sponsors_controller.rb +++ b/app/controllers/sponsors_controller.rb @@ -1,5 +1,11 @@ class SponsorsController < ApplicationController def index + # v1: bump when the sponsors view or partials change, otherwise a deploy + # keeps serving the cached body until the next sponsor save. + key = "sponsors/index/v1/#{Sponsor.active.maximum(:updated_at)&.to_fs(:usec)}" @sponsor_levels = Sponsor.active.group_by(&:level) + body = Rails.cache.fetch(key) { render_to_string(layout: false) } + # body is markup rendered by this app's own template, not user input + render html: body.html_safe # rubocop:disable Rails/OutputSafety end end diff --git a/spec/requests/sponsors_spec.rb b/spec/requests/sponsors_spec.rb new file mode 100644 index 000000000..6e8a75bec --- /dev/null +++ b/spec/requests/sponsors_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Sponsors' do + let!(:sponsor) { Fabricate.create(:sponsor, name: 'Acme Corp') } + + around do |example| + original_cache = Rails.cache + Rails.cache = ActiveSupport::Cache::MemoryStore.new + example.run + Rails.cache = original_cache + end + + it 'renders the sponsors page' do + get '/sponsors' + + expect(response).to have_http_status(:ok) + expect(response.body).to include('Acme Corp') + end + + it 'serves the cached body when no sponsor has been updated' do + get '/sponsors' + + # update_columns bypasses callbacks, so updated_at (and the cache key) stays unchanged + sponsor.update_columns(name: 'Renamed Corp') + + get '/sponsors' + + expect(response.body).to include('Acme Corp') + expect(response.body).not_to include('Renamed Corp') + end + + it 're-renders when a sponsor is updated' do + get '/sponsors' + + sponsor.update!(name: 'Renamed Corp') + + get '/sponsors' + + expect(response.body).to include('Renamed Corp') + end +end From 5d01c78b601c6b4582acdec929b013076598991a Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Sat, 19 Sep 2026 13:57:01 +0200 Subject: [PATCH 2/2] Move the sponsor load inside the cache block Warm requests no longer instantiate 721 AR objects they never use; the load only runs on a cache miss, so the warm path is one `maximum` query plus one cache read. Matches the corrected proposal in #2885. --- app/controllers/sponsors_controller.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/controllers/sponsors_controller.rb b/app/controllers/sponsors_controller.rb index 64db3a955..6452091b6 100644 --- a/app/controllers/sponsors_controller.rb +++ b/app/controllers/sponsors_controller.rb @@ -3,8 +3,10 @@ def index # v1: bump when the sponsors view or partials change, otherwise a deploy # keeps serving the cached body until the next sponsor save. key = "sponsors/index/v1/#{Sponsor.active.maximum(:updated_at)&.to_fs(:usec)}" - @sponsor_levels = Sponsor.active.group_by(&:level) - body = Rails.cache.fetch(key) { render_to_string(layout: false) } + body = Rails.cache.fetch(key) do + @sponsor_levels = Sponsor.active.group_by(&:level) + render_to_string(layout: false) + end # body is markup rendered by this app's own template, not user input render html: body.html_safe # rubocop:disable Rails/OutputSafety end