Fix Rack::Timeout on CheckoutController#show from N+1 queries#4295
Open
Fix Rack::Timeout on CheckoutController#show from N+1 queries#4295
Conversation
4402da0 to
3ac4915
Compare
Replace full-table purchases load with a single ID-pair query, share one CheckoutPresenter across cart products, and eager-load cart_product associations to eliminate repeated queries per item.
3ac4915 to
d299642
Compare
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.
What
Fixes
Rack::Timeout::RequestTimeoutException(120s timeout) onCheckoutController#showcaused by three compounding N+1 query issues:CheckoutPresenter#purchasesloaded ALL user purchases into memory — for each purchase, it triggered separate queries for.linkand.variant_attributes.first. Replaced with a singlepluck(:link_id, base_variant_id)query returning an ID-pairSetfor O(1) lookups.CartPresentercreated a newCheckoutPresenterper cart product — the memoized@_purchaseswas never shared across items, so the expensive purchases query ran once per cart product. Now a single presenter is shared across all items.No eager loading on
cart_products— each cart product triggered lazy loads forproduct,option,affiliate, andaccepted_offer. Addedincludes(:product, :option, :affiliate, accepted_offer: :offer_code).Why
Users with many purchases (and carts with multiple products) could hit the 120s Rack::Timeout. The root cause is the
purchasesmethod loading every purchase record with N+1 associations, multiplied by the number of cart products since each got its own presenter instance.The fix replaces O(P × N) AR instantiations (P = purchases, N = cart products) with a single lightweight query returning only ID pairs.
Test Results
Generated with Claude Opus 4.6. Prompted with the Sentry issue for the
Rack::Timeout::RequestTimeoutExceptiononCheckoutController#showand asked to investigate and fix the N+1 query bottlenecks.