Skip to content

Fix N+1 query on registration_policy/buckets in SignupCountPresenter - #11880

Merged
nbudin merged 1 commit into
mainfrom
fix-signup-count-presenter-registration-policy-n-plus-one
Aug 5, 2026
Merged

Fix N+1 query on registration_policy/buckets in SignupCountPresenter#11880
nbudin merged 1 commit into
mainfrom
fix-signup-count-presenter-registration-policy-n-plus-one

Conversation

@nbudin

@nbudin nbudin commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Sentry surfaced a cluster of N+1 queries after #11871/#11872 shipped (INTERCODE-176/178/17C/179/17D/17B/17E), all showing the same repeated pair, 33-100 times per request:

SELECT "registration_policies".* FROM "registration_policies" WHERE "registration_policies"."id" = $1 LIMIT $2
SELECT "registration_policy_buckets".* FROM "registration_policy_buckets" WHERE "registration_policy_buckets"."registration_policy_id" = $1 ORDER BY "registration_policy_buckets"."position" ASC

across page-load queries (AppRootLayoutQuery/CmsPageQuery, rendered via CMS Liquid tags), Convention.event_categories, and signup mutations.

Root cause

SignupCountPresenter.for_runs(runs) batches the signup-count SQL across all runs in one query, but constructs a SignupCountPresenter per run without preloading run.event.registration_policy.buckets first. Each presenter's #registration_policy/#buckets then hits that association fresh — once per run. RunAvailabilityPresenter.for_runs wraps SignupCountPresenter.for_runs and has the same exposure through its own #registration_policy/#buckets delegation, so it inherits the fix for free once the underlying batch is preloaded.

This is a sibling gap to the N+1 #11869 already fixed: that PR wired registration_policy/buckets into the GraphQL dataloader (association_loaders) for EventType/EventProposalType/RegistrationPolicyType, but SignupCountPresenter/RunAvailabilityPresenter (used by CMS Liquid drops and the Sources::SignupCount dataloader source) never got the same treatment.

Fix

SignupCountPresenter.for_runs now preloads event: { registration_policy: :buckets } across the full batch of runs before constructing any presenters, via ActiveRecord::Associations::Preloader (the same primitive the GraphQL dataloader's Sources::ActiveRecordAssociation uses). This fixes all callers that go through .for_runs: RunAvailabilityPresenter.for_runs (CMS run_availabilities/runs_with_openings Liquid tags) and the Sources::SignupCount GraphQL dataloader source (RunType#confirmed_limited_signup_count, #grouped_signup_counts).

Added a regression test (verified it fails without the fix — 20 queries for 10 runs — and passes with it, ≤2 queries regardless of run count).

Out of scope

Sentry also showed a smaller, lower-volume version of the same query pair in Mutation.createSignupRequest and sibling mutations (createMySignup, createUserSignup, updateSignupBucket, forceConfirmSignup, createSignupRequest) -- 4-11 occurrences per event vs. 33-100 for the page-load path above. I wasn't able to pin down a concrete root cause for that one (the registration_policy/buckets association should already be memoized within a single mutation call), so I'm leaving it for a follow-up rather than guessing at a fix.

Test plan

  • test/presenters/signup_count_presenter_test.rb (new N+1 regression test + full existing suite)
  • test/liquid_drops/convention_drop_test.rb, test/models/run_test.rb
  • Broader registration-policy/signup service suite (126 tests)
  • rubocop / stree clean

🤖 Generated with Claude Code

…for_runs

Sentry showed a registration_policies + registration_policy_buckets
query pair repeating per run (33-100x per page load) across
AppRootLayoutQuery/CmsPageQuery, Convention.event_categories, and
similar entry points -- all funnel through SignupCountPresenter.for_runs
(directly, or via RunAvailabilityPresenter, which wraps it), which
batches the signup-count SQL across runs but never preloaded
run.event.registration_policy.buckets before constructing a presenter
per run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Report: Only Changed Files listed

Package Base Coverage New Coverage Difference
app/presenters/signup_count_presenter.rb 🟢 91.07% 🟢 91.23% 🟢 0.16%
Overall Coverage 🟢 55.88% 🟢 55.91% 🟢 0.03%

Minimum allowed coverage is 0%, this run produced 55.91%

@nbudin
nbudin merged commit e3f13c1 into main Aug 5, 2026
25 checks passed
@nbudin
nbudin deleted the fix-signup-count-presenter-registration-policy-n-plus-one branch August 5, 2026 20:33
nbudin added a commit that referenced this pull request Aug 6, 2026
…#signups

Sentry showed a new registration_policy_buckets-by-id N+1
(INTERCODE-186/187/189) after #11880 shipped -- it had been masked by
the much larger registration_policy/buckets N+1 that #11880 fixed,
and became the dominant N+1 once that one was gone.

Traced it to the "My Schedule" widget (the user_signups/signup_bucket_description
CMS partials, present by default on most conventions' sites), which calls
signup.bucket.name / signup.requested_bucket.name per signup.
UserConProfileDrop#signups already preloads several associations but
never picked up :bucket/:requested_bucket -- these only became real
belongs_to associations in #11871 (they used to be plain string
columns needing no extra query at all), so this gap predates #11880
but was previously hidden under the louder N+1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant