perf(gateway): pool backend conns; loadtest evaluate hot path #101
Reference in New Issue
Block a user
Delete Branch "feature/loadtest-evaluate-hotpath"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Re-prepared and re-ran the pre-release load test against the current code, fixed the bottleneck it revealed, and trimmed the hot read path.
1. Harness now models the
game.evaluatehot pathA real client fires a debounced
game.evaluate(legality + score preview) on every tile placement while composing a play — several per turn, the single hottest gameplay call. The harness never modelled it; now it does (oneevaluateper placed tile + reconsideration +draft.save, human-paced).--eval=falsereproduces the pre-evaluate baseline. At 500 playersgame.evaluateis ~116 req/s.2. Fix: gateway → backend connection churn (the real bottleneck)
The gateway's backend REST client used the default
http.Transport(MaxIdleConnsPerHost = 2). Every sync call to the one backend host churned a TCP connection — ~26 500 TIME_WAIT sockets at 500 players, near the ephemeral-port ceiling, burning ~1.75 gateway cores while the backend sat near-idle. Widening the pool (backendMaxIdleConns = 512), measured before/after at 500 players:game.statep993. Postgres read path (latency win, honest about CPU)
game.evaluateno longer reads the DB on the hot path: an active game is warm in the live-game cache, so the preview checks membership against the cached seat list and scores the cached engine game — noGetGameon a warm hit.GetGamealso folds its two queries into oneLEFT JOIN. Result:game.evaluatep99 halved (200 → 100 ms) and per-op query count dropped.It does not cut postgres CPU —
pg_stat_user_tablesshows postgres is write-bound (per-moveCommitMove, draft upserts, journal replays), not read-bound, and it runs with headroom (~1.5 of 2 cores). So this is a latency / query-volume optimization, not a DB-CPU one; chasing the write path wasn't worth the risk for a service with headroom (and the gateway fix freed ~3 cores on the box if postgres ever needs more).4. Sizing — the "≈150 concurrent / 2-core" floor was the churn bug
The old "gateway is the binding constraint, scale horizontally" conclusion was sizing around the churn. Fixed, the app draws ~2.7 cores at 500 players (gateway ~0.25, backend ~0.77, postgres ~1.65), down from the ~5.5-core peak the tuning pass recorded under a lighter workload. Full analysis in
loadtest/REPORT.md.5. Docs
Consolidated the two trip reports into one
loadtest/REPORT.md(dropped the R2/R7 split); baked the findings intoREADME/PRERELEASE/ARCHITECTURE/TESTING.Tests
go build/vet,gofmt, gateway + loadtest + backend unit, and the integration suite (testcontainers Postgres, exercisingGetGame's join + evaluate across every game op) all green locally. Added regression guards: the backend pool stays wider than the stdlib default; a non-player evaluate asserts the cached-seat membership path. Verified live on the test contour (before/after runs above).