Cached polled-feed reponse in gameboard#21
Merged
Conversation
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.
Cache polled gameboard JSON feeds (response cache-aside, TTL + singleflight)
Summary
The gameboard polls four JSON feeds every 15s per connected client — activity, teams, challenges, and chat — and each poll ran several Postgres queries for data that's read-heavy and tolerates a few seconds of staleness. This adds a two-layer response cache-aside so feed responses are served from memory (and Redis) on hit, skipping the DB work entirely. It reuses the Redis connection that was previously used only for sessions.
Why
JSONCountriesHandler/JSONWorldDominationHandler/JSONTeamsHandlereach issue multiple DB queries (teams, scores, active challenges, categories, members) per poll; with N clients that's a lot of repeated identical queries.What changes
cmd/map/handlers/respcache.go: a per-feed-key, two-layer cache-aside.sync.Map, 4s TTL — coalesces a burst of simultaneous polls.mapctf:feed:<name>:<uuid>, 4s TTL — shared across instances.singleflightper key so a cache miss under concurrent polls does onebuild, and each waiter writes the shared result to its ownResponseWriter.serveCachedJSON(w, key, build): on a cache hit thebuildclosure is never invoked, so the DB queries inside it are skipped.200responses are cached; error responses are delivered but never cached (a transient DB error won't be stuck for 4s).buildreturns(status, alreadyMarshaledBody)— keeps per-handler translated error messages intact and lets singleflight safely share the result across separate writers.RedisCache.Client; if Redis is unavailable (feedCache()nil), feeds build and respond with no caching.cmd/map/handlers/json.go:JSONActivityHandler,JSONTeamsHandler,JSONChallengesHandler,JSONChatHandlerwrap their post-validation logic in aserveCachedJSONbuildclosure.validatedJSONUUIDstill runs first (cheap, no DB) so invalid UUIDs never get a cached response.cmd/map/handlers/handlers.go:feeds *respCachefield (lazily initialized fromRedisCache.Client).cmd/map/handlers/post.go: targeted invalidation on high-frequency gameplay mutations so captures feel responsive (the 4s TTL already bounds staleness, but invalidation makes it instant):teams+activityteams+activitychatcmd/map/handlers/json_test.go:TestJSONChallengesFeedCacheServesL1AndInvalidatesinjects an L1-onlyrespCache(network-free), proves a bypassing DB write is not reflected until the cache expires/invalidates, and thatinvalidateFeedrefreshes immediately.Not cached (intentional)
JSONCountriesHandler/JSONWorldDominationHandler— per-user (they depend on the current user's team); they need per-(uuid, teamID)keying with the teamID resolved up front. Follow-up.JSONGameClockHandler— time-based (ServerTime/remaining_mscomputed fromnow); caching would freeze the clock.Behavior preserved
Performance
singleflight(onebuildserves all concurrent waiters).teams/activityimmediately so the leaderboard/map reflect a solve on the next poll rather than up to 4s later.Risks / edge cases
invalidateFeedis a small follow-up if instant reflection is wanted.(uuid, teamID)keying follow-up lands.Testing
TestJSONChallengesFeedCacheServesL1AndInvalidates.go vet,gofmtclean.Rollout
Rebuild/restart the service. No schema or config change — the feed cache activates automatically when Redis is configured (as it already is for sessions).