Cache cms_settings reads instead of querying on every render - #194
Merged
Conversation
cms_settings is read on nearly every public page -- site title and footer on every layout, carousel and developing stories on the homepage -- and written a few times a month by an editor. Each read was its own round trip, so a handful of near-constant values became the dominant query load the moment the site went public, and with a small pool they queued in front of everything else. Raising the pool (#193) stopped the outage; this removes the reason the queue formed. Reads and writes now share one pair of helpers. The read caches per key with a 30s TTL; the write upserts and drops that key, so an editor's save is visible on the next request rather than up to a TTL later. Every writer moved onto the shared helper, including the startup backfill flag: leaving a single inline INSERT behind is exactly how a cache diverges from its table months later. Three details that decide whether a cache like this is correct: Errors are never cached. A failed read must not pin an empty value for the whole TTL and turn a blip into 30 seconds of wrong output. Absence is cached, and separately from emptiness. Callers distinguish "no row" (use the built-in default) from "row present but blank" (a deliberate empty value, e.g. breaking-news text), so the cache records both the value and whether the row existed. SETTINGS_CACHE_TTL_SECONDS=0 disables it outright, which is the escape hatch if a stale value is ever suspected of hiding a bug. Blue/green runs two processes; only the active slot serves traffic, so the standby's cache is cold rather than wrong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Follow-up to #193. That raised the pool and stopped the outage; this removes the reason the queue formed in the first place.
The problem
cms_settingsis read on nearly every public page — site title and footer on every layout, carousel and developing stories on the homepage — and written a few times a month by an editor. Every read was its own round trip. The moment www went public, a handful of near-constant values became the dominant query load, and with a ten-connection pool they queued in front of everything else:/v1/settings/sitewas taking 25–125 s while the database itself sat at two running threads.The change
Reads and writes share one pair of helpers in
settings_cache.go. The read caches per key with a 30 s TTL; the write upserts and drops that key, so an editor's save shows up on the next request rather than up to a TTL later.Every writer moved onto the shared helper — including the startup
articles_seo_backfilledflag. That one is written once and never read hot, but leaving a single inlineINSERTbehind is exactly how a cache quietly diverges from its table months later. There are now no directcms_settingsreads or writes outside this file.Three details that decide whether this is correct
SETTINGS_CACHE_TTL_SECONDS=0disables it outright, the escape hatch if a stale value is ever suspected of hiding a bug.Blue/green runs two processes, but only the active slot serves traffic, so the standby's cache is cold rather than wrong.
Tests
Six new unit tests, including one that passes a nil
*sql.DBto a cached read — if the cache ever falls through to a query it panics, rather than appearing to pass because a real connection happened to be available. Also covers TTL parsing (default / override /0/ invalid), cached absence, invalidation, expiry, and that a disabled cache stores nothing.Full suite green:
go build ./...,go vet ./...,go test ./....🤖 Generated with Claude Code