Skip to content

Per user feed cached#22

Merged
javuto merged 1 commit into
developfrom
per-user-feeds-cached
Jun 30, 2026
Merged

Per user feed cached#22
javuto merged 1 commit into
developfrom
per-user-feeds-cached

Conversation

@javuto

@javuto javuto commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Cache per-user gameboard feeds (countries + world domination) per team

Summary

The two remaining uncached gameboard feeds — JSONCountriesHandler and JSONWorldDominationHandler — are per-user (their solved_by_current / current-team metrics depend on the viewer's team), so they couldn't share a per-UUID cache entry like the global feeds. This caches them per (uuid, teamID), so the heavy per-poll DB work is skipped on a cache hit while teammates correctly share a result and different teams never see each other's view.

Why

  • These are the heaviest feeds: JSONCountriesHandler runs countries + active challenges + categories + all teams + all team scores + assembly; JSONWorldDominationHandler runs active challenges + current team + all scores + win/loss computation. Each polled every 15s per client.
  • Only the solved_by_current (countries) and current-team metrics (domination) depend on the team — everything else is per-UUID — but caching the whole response per team is simpler and bounded (short TTL).
  • Completes the feed caching started in the per-UUID feeds PR; all six polled feeds are now cached, with JSONGameClockHandler deliberately uncached (time-based).

What changes

  • cmd/map/handlers/json.go: both handlers resolve the current user's team up front (one Users.Get, hoisted from inside the handler — not an added query) and cache the response under mapctf:feed:countries:<uuid>:<teamID> / mapctf:feed:domination:<uuid>:<teamID>.
    • The full DB-query + assembly logic moves into a serveCachedJSON build closure, so a cache hit runs zero of those queries.
    • Teammates (same teamID) share one cache entry; users with no team key under :0; different teams get separate entries → no cross-team leakage.
    • Reuses the existing L1 (in-process, 4s) + L2 (Redis, 4s) + singleflight machinery from respcache.go; only 200 responses are cached; Redis-down degrades to uncached reads.
  • cmd/map/handlers/json_test.go: TestJSONWorldDominationFeedCacheKeysPerTeam — proves per-team separation (Alice's team's "1 completed" does not leak into Bob's "0") and that a bypassing DB write is not reflected while the L1 entry is fresh.

Intentional: TTL-only (no explicit invalidation)

Per-team keys can't be invalidated with a single DEL (would need a prefix scan over all team keys), so these two feeds rely on the 4s TTL for staleness. A capture reflects on the map/domination panel within ~4s — acceptable given the 15s poll cadence. The per-UUID global feeds (teams/activity/chat) keep their explicit invalidateFeed calls on score/hint/chat-post for instant updates.

Behavior preserved

  • Response bodies/statuses unchanged on cache miss (same marshaling path); validatedJSONUUID and the manager nil-checks still run before the cache lookup so invalid UUIDs / uninitialized managers never get a cached response.
  • With caching disabled (no Redis, e.g. existing tests), both feeds build and respond directly — no behavior change, no staleness. Existing countries/domination tests pass unchanged.

Performance

  • A countries/domination poll goes from ~5-6 DB queries per client to one Users.Get (indexed) + cache hit, per ~4s per (uuid, teamID), shared across teammates and across instances via Redis.
  • The up-front Users.Get is the key-derivation cost; it's an indexed lookup and far cheaper than the feed's queries it replaces on a hit.

Risks / edge cases

  • Staleness: up to 4s for countries/domination (no explicit invalidation); bounded by TTL.
  • Memory: per-team entries hold the full country map each; with short TTL and a modest team count this is bounded (a few MB on Redis).
  • Redis down: feeds degrade transparently to uncached DB reads.

Testing

  • New: TestJSONWorldDominationFeedCacheKeysPerTeam (L1-only, network-free).
  • Existing feed/handler suites pass (they run uncached).
  • Build, go vet, gofmt, golangci-lint (errorlint/gocritic/misspell) all clean.

Rollout

Rebuild/restart the service. No schema or config change — the per-user feed cache activates automatically when Redis is configured (as it already is for sessions).

@javuto javuto added ✨ enhancement New feature or request cache Cache related issues labels Jun 29, 2026
@javuto javuto merged commit 437b1df into develop Jun 30, 2026
1 check passed
@javuto javuto deleted the per-user-feeds-cached branch June 30, 2026 05:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cache Cache related issues ✨ enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant