Replace per-request settings DB polling with cache generation counter#10995
Open
pablomh wants to merge 1 commit into
Open
Replace per-request settings DB polling with cache generation counter#10995pablomh wants to merge 1 commit into
pablomh wants to merge 1 commit into
Conversation
…eration counter Every HTTP request triggered a SELECT on the settings table to detect changes (before_action :load_settings). At 912+ concurrent registrations, this generated 488K sequential scans on a 7-row table, with avg query time inflating from 0.9ms to 20.6ms under contention (23x). Replace the per-request DB poll with a Rails.cache generation counter: - Setting#after_commit bumps a shared cache counter (after_commit, not after_save, to avoid a race where another process observes the bump before the transaction commits) - SettingRegistry#load_values checks the counter before querying DB - Generation mismatch triggers a full reload of all settings (single coherence mechanism — no split between generation and updated_at) - 30-second staleness fallback: reload from DB even if generation appears current, to guard against lost cache writes during transient Redis outages - Works with both Redis and file-based cache stores - Falls back to DB polling if cache is unavailable Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
aa6fd4d to
f9bc228
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.
Summary
Replace the per-request
SELECT * FROM settings WHERE updated_at >= ...with a shared cache generation counter that skips the DB entirely when nothing has changed.Problem
Every HTTP request triggers
before_action :load_settings, which executes a query against thesettingstable to detect changes. During registration performance testing at 912+ concurrent registrations:settings99.99% of these queries return zero rows — settings almost never change during normal operation.
Design
Two-tier cache with a safety net:
Setting#after_saveupdates the in-memorySettingPresenterdirectly (existing behavior, unchanged)Setting#after_commitbumps a sharedRails.cachegeneration counter. Other Puma workers check the counter before querying DB — if it matches their last-seen value, skip the query entirely.The generation counter is the sole cross-process coherence signal — no split between generation and
updated_atwatermarks. On generation mismatch, all settings are reloaded (7 rows — negligible cost).Why
after_commit, notafter_saveafter_savefires before the transaction commits. Another worker could see the bumped generation, query the DB, get the old value (transaction not yet visible), cache it as seen, and serve stale settings indefinitely.after_commitensures the generation only bumps after the new value is visible to all readers.Cache backend compatibility
Works with both
:redis_cache_store(atomicincrement) and:file_store(non-atomic but safe — worst case is one extra DB poll, not stale data). Falls back to DB polling if cache is entirely unavailable.Changes
app/models/setting.rb: Addafter_commit :invalidate_settings_generationcallbackapp/registries/setting_registry.rb:load_values: check generation counter before querying DBreload_required?: extracted predicate (ignore_cache → nil check → staleness → generation)increment_generation!: class method to bump the shared counterstale?: 30-second fallback guardupdated_atfiltering)Test plan
ignore_cache: trueFound via
pg_stat_user_tablesduring registration performance testing at 1368 concurrent registrations with SQL debug logging enabled.🤖 Generated with Claude Code