Fix N+1 query problem in status() with StatusContext pattern#19
Fix N+1 query problem in status() with StatusContext pattern#19
Conversation
The status() method had severe query redundancy. When calling plan_owner.limits with 3 limits, the gem executed: - 18+ limit_status() calls instead of 3 - 72+ effective_plan_for() calls instead of 1 - ~70 database queries for a single call This happened because helper methods like severity_for(), message_for(), overage_for(), highest_severity_for() all re-called limit_status() internally, and each limit_status() call independently resolved the plan. Solution: StatusContext pattern - Request-scoped context object created at start of status() - Caches: plan resolution, limit configs, usage, percent used, grace state, enforcement states, severity, period windows - Thread-safe by design (each call gets its own context) - Implements period window calculation inline to avoid extra queries - Backwards compatible (existing public API unchanged) Performance improvement: - Total queries: ~70 → 16 (77% reduction) - enforcement_states queries: 6 → 3 (50% reduction) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Code Review: Fix N+1 Query Problem with StatusContext PatternSummaryThis is an excellent performance optimization that addresses a severe N+1 query problem. The implementation is well-designed, thread-safe, and backwards-compatible. The 77% reduction in queries (from ~70 to 16) is substantial and will have meaningful impact on production performance. ✅ StrengthsArchitecture & Design
Performance
Code Quality
🔍 Issues & Recommendations1. CRITICAL: Side Effect Removed in
|
Adds comprehensive tests for the new StatusContext class: - Caching behavior (plan, config, status, severity) - limit_status for configured and unconfigured limits - severity_for at different usage levels - highest_severity_for aggregation - message_for at different severities - overage_for calculation - period_window_for per-period limits - grace and blocking behavior Branch coverage: 65.4% (above 65% threshold) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pull Request Review: Fix N+1 Query Problem with StatusContext PatternSummaryThis PR introduces a StatusContext pattern to eliminate severe query redundancy in the ✅ Strengths1. Excellent Problem Identification & Solution Design
2. Strong Performance Gains
3. Backwards Compatibility
4. Code Quality
🔍 Issues & RecommendationsCRITICAL: Logic Duplication & Maintenance RiskThe
Problem: When Recommendation: Refactor to eliminate duplication: # Instead of duplicating calculate_period_window, call PeriodCalculator directly
# but pass cached values to avoid re-resolving the plan
def period_window_for(limit_key)
key = limit_key.to_sym
@period_window_cache ||= {}
return @period_window_cache[key] if @period_window_cache.key?(key)
limit_config = limit_config_for(limit_key)
return @period_window_cache[key] = [nil, nil] unless limit_config && limit_config[:per]
# Call PeriodCalculator but pass config to avoid re-lookup
@period_window_cache[key] = PeriodCalculator.calculate_window(
@plan_owner,
limit_config[:per],
# Pass any other needed cached values
)
endConsider adding a MEDIUM: Incomplete Replacement in status()Line 342 in lib/pricing_plans.rb: msg = compute_summary_message_from_context(ctx, keys, sev)The PR introduces Issues:
Recommendation:
MEDIUM: Test Coverage GapsThe new tests in
Recommendation: Add: def test_concurrent_status_calls_are_independent
org = create_organization
threads = 5.times.map do
Thread.new { PricingPlans.status(org, limits: [:projects, :custom_models]) }
end
results = threads.map(&:value)
# Each should have independent context, no shared state
assert_equal 5, results.size
endMINOR: Documentation & Comments
MINOR: Potential Memory ConcernWith 8 separate cache hashes ( Recommendation: Add a note in documentation about expected usage patterns, or consider a single unified cache hash. 🔒 Security Considerations✅ No security concerns identified
🧪 Testing RecommendationsBeyond the test coverage gaps mentioned above:
📋 Checklist Before Merge
Final VerdictRecommendation: Approve with changes requested This PR solves a real performance problem with a solid architectural approach. The 77% query reduction is excellent. However, the logic duplication is a significant maintenance concern that should be addressed before merging. The StatusContext pattern itself is sound - the implementation just needs refactoring to reuse existing logic rather than duplicating it. Once the duplication is resolved and test coverage is improved, this will be a strong addition to the codebase. Estimated effort to address: 2-4 hours of refactoring work. Great work identifying and fixing this performance issue! 🚀 |
…grace_ends_at fix 1. CRITICAL: Added state.destroy! in fresh_enforcement_state to clean up stale enforcement states (consistent with GraceManager behavior) 2. MEDIUM: Removed ~80 lines of duplicated period calculation code from StatusContext. Added PeriodCalculator.window_for_period_type() to accept pre-resolved period_type, avoiding redundant plan lookups 3. MEDIUM: Changed grace_ends_at to use fresh_enforcement_state instead of enforcement_state to avoid returning stale data 4. Added comprehensive tests for new behaviors including stale state cleanup, grace period handling, message generation branches, and caching Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR Review: Fix N+1 Query Problem with StatusContext PatternSummaryThis PR introduces a well-designed caching pattern to eliminate severe query redundancy in the status() method. The implementation achieves a 77% reduction in database queries (from ~70 to 16) - an impressive performance improvement. ✅ Strengths1. Excellent Problem IdentificationThe PR clearly identifies and documents the N+1 query problem where helper methods like severity_for(), message_for(), and overage_for() were internally re-calling limit_status(), causing redundant database queries and plan resolutions. 2. Clean Architecture
3. Comprehensive Caching StrategyThe StatusContext class caches all computed values appropriately including plan resolution, limit configurations, usage counts, grace states, severity calculations, warning thresholds, and period windows. 4. Excellent Test CoverageThe PR includes 459 lines of comprehensive tests covering caching behavior verification, edge cases (stale states, unconfigured limits, unlimited limits), all severity levels, message generation, and proper destruction of stale enforcement states. 5. Performance ResultsDocumented performance improvements are significant:
🔍 Areas for Improvement1. Potential Memory Overhead (Severity: Low)Each StatusContext instance maintains 10+ cache hashes. For endpoints that check many limits simultaneously, this could create temporary memory pressure. This is acceptable for the use case, but consider documenting the memory trade-off in comments. 2. Stale State Destruction Side Effect (Severity: Medium)In fresh_enforcement_state (lib/pricing_plans/status_context.rb:404-435), stale enforcement states are destroyed as a side effect of a read operation. This couples read operations with write operations (database DELETE), which could cause unexpected behavior in read-only contexts, make debugging harder, and potentially trigger issues in read-replica setups. Recommendation: Consider moving cleanup to a separate maintenance job, adding a configuration flag to control auto-cleanup behavior, or at minimum, document this behavior clearly. 3. Code Duplication (Severity: Low)The compute_summary_message_from_context method duplicates logic from message_for in StatusContext. Both build similar human-readable messages with sentence joining logic. Consider extracting common message-building logic into a shared helper method. 4. Missing Error Handling (Severity: Low)In message_for, there's a silent rescue for custom message builders. Silent failures make debugging difficult. Recommendation: Log the error before falling through. 5. Period Window Caching Initialization (Severity: Very Low)In period_window_for, the cache is conditionally initialized (@period_window_cache ||= {}) which is inconsistent with other caches initialized in the constructor. Initialize @period_window_cache = {} in the constructor for consistency. 🔒 Security Considerations✅ No security concerns identified The PR doesn't introduce security vulnerabilities - no user input is directly used in queries, no authentication/authorization changes, caching is request-scoped (no cross-user data leakage), and database queries use existing safe patterns. 🧪 Testing RecommendationsThe test coverage is excellent, but consider adding:
🎯 Overall AssessmentRecommendation: APPROVE with minor suggestions This is a high-quality PR that solves a real performance problem with a clean, well-tested solution. The StatusContext pattern is an appropriate choice - it's simple, effective, and maintains backwards compatibility. What I Like Most
Priority Actions
The PR is ready to merge as-is, but addressing the stale state destruction concern would make it even more robust for production use. Great work on this optimization! The 77% query reduction will significantly improve response times. 🤖 Review generated with Claude Code |
Admin overrides (manual assignments) should take precedence over subscription-based plans. The previous order checked Pay subscriptions first, which meant manual assignments were ignored for users with active subscriptions - defeating the purpose of admin overrides. New resolution order: 1. Manual assignment (admin override) - FIRST 2. Pay subscription 3. Default plan Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This reverts commit 8bee47d.
PR Review: Fix N+1 Query Problem with StatusContext PatternSummaryThis is an excellent performance optimization that introduces a well-designed caching pattern to eliminate severe N+1 query issues. The implementation is solid, comprehensive, and follows Rails best practices. ✅ Strengths1. Architecture & Design
2. Performance Impact 🚀
3. Code Quality
4. Important Bug Fixes
🔍 Observations & Recommendations1. Potential Circular Dependency (Minor)Location: lib/pricing_plans/status_context.rb:310 The compute_limit_status → grace_ends_at → fresh_enforcement_state → period_window_for chain could be optimized. Consider pre-computing fresh_enforcement_state once and passing it to dependent methods to avoid potential cache checking overhead. Recommendation: Not critical, but could micro-optimize by computing fresh_enforcement_state first in compute_limit_status. 2. Stale State Logic Complexity (Minor)Location: lib/pricing_plans/status_context.rb:280-282 The third condition window_start_epoch != current_epoch is redundant when combined with the second. If window_start_epoch < current_epoch, it's already !=. The third condition only adds value if window_start_epoch > current_epoch, which would be a future period (shouldn't happen). Recommendation: Consider simplifying to: 3. Cache Initialization Pattern (Style)Location: lib/pricing_plans/status_context.rb:124 All other caches are initialized in the constructor, but @period_window_cache is lazy-initialized. For consistency, consider initializing in the constructor. 4. Error Swallowing (Low Risk)Location: lib/pricing_plans/status_context.rb:190-192 Silent error swallowing could hide bugs in custom message_builder. Consider logging errors (if debug mode enabled) to aid troubleshooting. 5. Test Coverage ✅
Excellent test coverage! 🔒 Security Considerations✅ No security issues identified
📊 Performance Considerations✅ Outstanding performance improvement
Potential optimization: Consider adding database indexes on enforcement_states(plan_owner_type, plan_owner_id, limit_key) if not already present, as fresh_enforcement_state queries by these columns. 🎯 Breaking Changes✅ Breaking change is justified and well-documented The manual assignment precedence change (lib/pricing_plans/plan_resolver.rb:13) is technically breaking, but:
Recommendation: Consider mentioning this in release notes/changelog as it changes resolution order. 📝 DocumentationSuggestions:
✅ Final VerdictAPPROVE ⭐⭐⭐⭐⭐ This is high-quality work that demonstrates:
The minor observations above are style/optimization suggestions, not blockers. This PR is ready to merge. Great work! This will significantly improve performance for users checking multiple limits. Checklist Summary
Recommendation: Merge after considering the minor optimization suggestions above. |
Summary
status()method where helper methods re-calledlimit_status()internallyStatusContextpattern that caches all computed values within a singlestatus()callProblem
When calling
plan_owner.limits(which callsstatus()) with 3 limits (products, licenses, activations), the gem was executing:limit_status()calls instead of 3effective_plan_for()calls instead of 1This happened because helper methods like
severity_for(),highest_severity_for(),message_for(), etc. all re-calledlimit_status()internally, and eachlimit_status()call independently resolved the plan, counted usage, and checked grace state.Solution: StatusContext Pattern
Created a request-scoped context object that caches all computed values within a single
status()call:The context caches:
effective_plan- plan resolution (called once)limit_config_for(key)- limit configuration lookuplimit_status(key)- full status hash per limitcurrent_usage_for(key)- usage countgrace_active?(key)- grace period checkshould_block?(key)- block checkseverity_for(key)- computed from cached dataperiod_window_for(key)- period window calculationPerformance Results
limit_status()callseffective_plan_for()callsTest plan
🤖 Generated with Claude Code