R2: load-test harness + contour resource observability
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 38s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Failing after 3s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 38s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Failing after 3s
New scrabble/loadtest module (the pre-release stress harness): seeds 1000 guest + 10000 durable accounts with pre-created sessions directly in Postgres (token hash matches backend/internal/session), drives virtual players through the edge protocol (real 2-4p games assembled via invitations, mid-ranked legal moves generated locally by the embedded scrabble-solver — the edge carries no board, so the client replays history), plus nudge/chat/check-word/draft/profile/stats and a gateway-hammer that verifies the rate limiter. Prints a trip-report summary (per-op latency percentiles, result codes, live-event tally). Go unit tests cover the pure pieces; the DAWG-backed move test runs under BACKEND_DICT_DIR. Contour: add cAdvisor + postgres_exporter + a 'Scrabble - Resources' Grafana dashboard and the two Prometheus scrape jobs, for the R2/R7 stress-run resource baseline. CI: gate ./loadtest/... (path filter + vet/build/test). Docs: TESTING, ARCHITECTURE, project CLAUDE repo layout.
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
package edge
|
||||
|
||||
import (
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
|
||||
fb "scrabble/pkg/fbs/scrabblefb"
|
||||
)
|
||||
|
||||
// PlayTile is one tile to place, addressed by alphabet index (255 marks a blank's
|
||||
// carrier letter together with Blank=true), as the submit-play request carries it.
|
||||
type PlayTile struct {
|
||||
Row, Col int
|
||||
Letter byte
|
||||
Blank bool
|
||||
}
|
||||
|
||||
// gameAction builds a GameActionRequest payload (just a game id): pass, nudge,
|
||||
// history, draft.get.
|
||||
func gameAction(gameID string) []byte {
|
||||
b := flatbuffers.NewBuilder(64)
|
||||
gid := b.CreateString(gameID)
|
||||
fb.GameActionRequestStart(b)
|
||||
fb.GameActionRequestAddGameId(b, gid)
|
||||
b.Finish(fb.GameActionRequestEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// stateReq builds a StateRequest payload. includeAlphabet asks the backend to embed
|
||||
// the variant alphabet table (the driver sets it once per variant).
|
||||
func stateReq(gameID string, includeAlphabet bool) []byte {
|
||||
b := flatbuffers.NewBuilder(64)
|
||||
gid := b.CreateString(gameID)
|
||||
fb.StateRequestStart(b)
|
||||
fb.StateRequestAddGameId(b, gid)
|
||||
fb.StateRequestAddIncludeAlphabet(b, includeAlphabet)
|
||||
b.Finish(fb.StateRequestEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// submitPlay builds a SubmitPlayRequest payload. dir is "H" or "V"; tiles are the
|
||||
// newly-placed tiles in main-word order.
|
||||
func submitPlay(gameID, dir string, tiles []PlayTile) []byte {
|
||||
b := flatbuffers.NewBuilder(256)
|
||||
gid := b.CreateString(gameID)
|
||||
d := b.CreateString(dir)
|
||||
offs := make([]flatbuffers.UOffsetT, len(tiles))
|
||||
for i, t := range tiles {
|
||||
fb.PlayTileStart(b)
|
||||
fb.PlayTileAddRow(b, int32(t.Row))
|
||||
fb.PlayTileAddCol(b, int32(t.Col))
|
||||
fb.PlayTileAddLetter(b, t.Letter)
|
||||
fb.PlayTileAddBlank(b, t.Blank)
|
||||
offs[i] = fb.PlayTileEnd(b)
|
||||
}
|
||||
fb.SubmitPlayRequestStartTilesVector(b, len(offs))
|
||||
for i := len(offs) - 1; i >= 0; i-- {
|
||||
b.PrependUOffsetT(offs[i])
|
||||
}
|
||||
tilesVec := b.EndVector(len(offs))
|
||||
fb.SubmitPlayRequestStart(b)
|
||||
fb.SubmitPlayRequestAddGameId(b, gid)
|
||||
fb.SubmitPlayRequestAddDir(b, d)
|
||||
fb.SubmitPlayRequestAddTiles(b, tilesVec)
|
||||
b.Finish(fb.SubmitPlayRequestEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// exchange builds an ExchangeRequest payload swapping the listed rack tiles (alphabet
|
||||
// indices; 255 a blank).
|
||||
func exchange(gameID string, tiles []byte) []byte {
|
||||
b := flatbuffers.NewBuilder(64)
|
||||
gid := b.CreateString(gameID)
|
||||
vec := b.CreateByteVector(tiles)
|
||||
fb.ExchangeRequestStart(b)
|
||||
fb.ExchangeRequestAddGameId(b, gid)
|
||||
fb.ExchangeRequestAddTiles(b, vec)
|
||||
b.Finish(fb.ExchangeRequestEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// checkWord builds a CheckWordRequest payload (alphabet indices for the word).
|
||||
func checkWord(gameID string, word []byte) []byte {
|
||||
b := flatbuffers.NewBuilder(64)
|
||||
gid := b.CreateString(gameID)
|
||||
vec := b.CreateByteVector(word)
|
||||
fb.CheckWordRequestStart(b)
|
||||
fb.CheckWordRequestAddGameId(b, gid)
|
||||
fb.CheckWordRequestAddWord(b, vec)
|
||||
b.Finish(fb.CheckWordRequestEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// chatPost builds a ChatPostRequest payload.
|
||||
func chatPost(gameID, body string) []byte {
|
||||
b := flatbuffers.NewBuilder(128)
|
||||
gid := b.CreateString(gameID)
|
||||
bd := b.CreateString(body)
|
||||
fb.ChatPostRequestStart(b)
|
||||
fb.ChatPostRequestAddGameId(b, gid)
|
||||
fb.ChatPostRequestAddBody(b, bd)
|
||||
b.Finish(fb.ChatPostRequestEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// draftSave builds a DraftRequest payload carrying the opaque composition JSON.
|
||||
func draftSave(gameID, jsonStr string) []byte {
|
||||
b := flatbuffers.NewBuilder(128)
|
||||
gid := b.CreateString(gameID)
|
||||
j := b.CreateString(jsonStr)
|
||||
fb.DraftRequestStart(b)
|
||||
fb.DraftRequestAddGameId(b, gid)
|
||||
fb.DraftRequestAddJson(b, j)
|
||||
b.Finish(fb.DraftRequestEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// updateProfile builds an UpdateProfileRequest payload. It resends the marker display
|
||||
// name and sane defaults so the account stays findable by the seeder's Cleanup.
|
||||
func updateProfile(displayName, lang string) []byte {
|
||||
b := flatbuffers.NewBuilder(192)
|
||||
name := b.CreateString(displayName)
|
||||
pl := b.CreateString(lang)
|
||||
tz := b.CreateString("UTC")
|
||||
as := b.CreateString("00:00")
|
||||
ae := b.CreateString("07:00")
|
||||
fb.UpdateProfileRequestStart(b)
|
||||
fb.UpdateProfileRequestAddDisplayName(b, name)
|
||||
fb.UpdateProfileRequestAddPreferredLanguage(b, pl)
|
||||
fb.UpdateProfileRequestAddTimeZone(b, tz)
|
||||
fb.UpdateProfileRequestAddAwayStart(b, as)
|
||||
fb.UpdateProfileRequestAddAwayEnd(b, ae)
|
||||
fb.UpdateProfileRequestAddBlockChat(b, false)
|
||||
fb.UpdateProfileRequestAddBlockFriendRequests(b, false)
|
||||
fb.UpdateProfileRequestAddNotificationsInAppOnly(b, true)
|
||||
b.Finish(fb.UpdateProfileRequestEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// createInvitation builds a CreateInvitationRequest payload. turnTimeoutSecs 0 asks
|
||||
// the backend for its default; dropoutTiles "remove" is the standard policy.
|
||||
func createInvitation(inviteeIDs []string, variant string, turnTimeoutSecs int) []byte {
|
||||
b := flatbuffers.NewBuilder(256)
|
||||
idOffs := make([]flatbuffers.UOffsetT, len(inviteeIDs))
|
||||
for i, id := range inviteeIDs {
|
||||
idOffs[i] = b.CreateString(id)
|
||||
}
|
||||
fb.CreateInvitationRequestStartInviteeIdsVector(b, len(idOffs))
|
||||
for i := len(idOffs) - 1; i >= 0; i-- {
|
||||
b.PrependUOffsetT(idOffs[i])
|
||||
}
|
||||
ids := b.EndVector(len(idOffs))
|
||||
variantOff := b.CreateString(variant)
|
||||
dropout := b.CreateString("remove")
|
||||
fb.CreateInvitationRequestStart(b)
|
||||
fb.CreateInvitationRequestAddInviteeIds(b, ids)
|
||||
fb.CreateInvitationRequestAddVariant(b, variantOff)
|
||||
fb.CreateInvitationRequestAddTurnTimeoutSecs(b, int32(turnTimeoutSecs))
|
||||
fb.CreateInvitationRequestAddHintsAllowed(b, true)
|
||||
fb.CreateInvitationRequestAddHintsPerPlayer(b, 1)
|
||||
fb.CreateInvitationRequestAddDropoutTiles(b, dropout)
|
||||
b.Finish(fb.CreateInvitationRequestEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// invitationAction builds an InvitationActionRequest payload (accept / decline /
|
||||
// cancel by id).
|
||||
func invitationAction(invitationID string) []byte {
|
||||
b := flatbuffers.NewBuilder(64)
|
||||
id := b.CreateString(invitationID)
|
||||
fb.InvitationActionRequestStart(b)
|
||||
fb.InvitationActionRequestAddInvitationId(b, id)
|
||||
b.Finish(fb.InvitationActionRequestEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// enqueueReq builds an EnqueueRequest payload (join the per-variant auto-match pool).
|
||||
func enqueueReq(variant string) []byte {
|
||||
b := flatbuffers.NewBuilder(64)
|
||||
v := b.CreateString(variant)
|
||||
fb.EnqueueRequestStart(b)
|
||||
fb.EnqueueRequestAddVariant(b, v)
|
||||
b.Finish(fb.EnqueueRequestEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
Reference in New Issue
Block a user