Mechanical, behaviour-preserving removal of Stage N / TODO-N / phase (RN) references from comments, doc-comments, service READMEs, the current-state docs (ARCHITECTURE, FUNCTIONAL+_ru, TESTING, UI_DESIGN), config-file comments, and the .fbs/.proto schema comments. PLAN.md / PRERELEASE.md / CLAUDE.md keep the stage history. - Rename the only stage-named identifiers: registerStage8 -> registerSocialOps, registerStage11 -> registerLinkOps (gateway transcode). - Split stage6_test.go: TestEmailLoginFlow -> email_test.go, TestGuestAutoMatchLeavesNoStats (+ provisionGuest) -> account_test.go. - Regenerated proto bindings (push.pb.go, telegram_grpc.pb.go) from the de-staged .proto comments; FB Go/TS bindings unchanged (flatc strips schema comments). go build/vet/gofmt clean across modules; integration typecheck and pnpm check green.
4.6 KiB
loadtest — stress harness
Reusable load harness for the pre-release stress pass. It seeds a large account population with pre-created sessions, drives virtual players through the gateway edge protocol in realistic games, hammers the rate limiter, and prints a trip-report summary. It stays in the repo for repeats.
What it does
- Seed (direct Postgres, schema
backend): inserts--durabledurable accounts (each with a confirmed email identity) +--guestguest accounts and an activesessionsrow per account, then hands the plaintext bearer tokens to the driver. Token hashes matchbackend/internal/session(hex(sha256(token))), so the seeded sessions resolve. Every row carries a distinctive display-name marker for cleanup. - Drive (edge protocol over h2c): assembles real 2–4 player games via the
invitation flow (
invitation.create→invitation.accept, no robots), then runs each player's turn loop — pollgame.state, replaygame.history, generate a legal mid-ranked move with the embeddedscrabble-solver, andgame.submit_play(or pass/exchange). A fraction of turns exercise nudge / chat / check-word / draft / profile-update / stats. Each player also holds a liveSubscribestream. The moderate ramp is 50 → 200 → 500 concurrent players, ~12 min per step. - Hammer: drives
games.listfrom one account far above the per-user rate limit to verify the limiter holds (rate_limitedresults) and measure its cost. - Report: per-operation latency percentiles, throughput, result-code breakdown, live-event tally and the aggregate error rate.
The driver runs the solver locally because the edge protocol carries no board: the client reconstructs it from decoded history (the same invariant as the UI).
Connection model
The harness reaches Postgres and the gateway directly, so run it as a one-shot container on the contour's docker network (this bypasses the host→gateway hairpin):
# from the repo root
docker build -f loadtest/Dockerfile -t scrabble-loadtest .
docker run --rm --name scrabble-loadtest --network scrabble-internal \
-e POSTGRES_PASSWORD="$TEST_POSTGRES_PASSWORD" \
scrabble-loadtest run
Defaults assume the contour service names: postgres:5432 and gateway:8081. The
DAWGs are baked into the image (/opt/dawg, pinned to the dictionary release). Run with
--name scrabble-loadtest so the harness's own CPU/memory show up as a scrabble-*
series in cAdvisor (keeping it separable from the system under test). Capture the
resource baseline from the Grafana Scrabble — Resources dashboard
(cAdvisor + postgres_exporter) while the run is in progress.
Commands & flags
loadtest run [flags] seed, drive the ramp + hammer, print the report
loadtest cleanup [flags] delete everything the harness seeded (matched by the display-name marker)
Key run flags (env in parentheses):
| flag | default | meaning |
|---|---|---|
--gateway (LOADTEST_GATEWAY_URL) |
http://gateway:8081 |
gateway base URL |
--dsn (LOADTEST_DSN) |
from POSTGRES_* |
backend Postgres DSN (schema backend) |
--dawg (LOADTEST_DAWG_DIR) |
/dawg (image: /opt/dawg) |
committed *.dawg directory |
--durable / --guest |
10000 / 1000 |
accounts to seed |
--steps |
50,200,500 |
concurrent-player ramp steps |
--step-dur |
12m |
hold time per step |
--games-per-player |
0 (random 3–5) |
target concurrent games per player |
--tick |
800ms |
per-player op cadence (keeps a player under the per-user limit) |
--secondary-prob |
0.08 |
chance per tick of a non-move op |
--hammer-workers / --hammer-dur |
20 / 15s |
gateway-hammer (0 workers disables) |
--reset / --cleanup |
false |
delete harness rows before / after the run |
run re-seeds every time (plaintext tokens are never stored), so pass --reset to
clear a prior run's rows first. The authoritative hard reset of the contour remains the
DB wipe (DROP SCHEMA backend CASCADE + backend restart).
Build & test
go build ./loadtest/...
go vet ./loadtest/...
BACKEND_DICT_DIR=../scrabble-solver/dawg go test -count=1 ./loadtest/...
The DAWG-backed moves test runs only when BACKEND_DICT_DIR is set (as the engine
tests use); the pure logic (hashing, board replay, rack build, move selection, report)
runs unconditionally.
Caveat
The harness shares the host CPU with the contour, so the early-pass resource baseline is read with the harness's own container series in mind; a cleaner number on separate hardware is future work. The moderate ramp keeps the generator from being the bottleneck.