Files
scrabble-game/loadtest/internal/scenario/hammer.go
T
Ilia Denisov 04263a17ca R7: per-player transports + drop finished games in the load harness
Each virtual player now builds its own edge.Client (its own h2c connection
carrying both the Subscribe stream and the Execute calls), instead of every
player multiplexing over a single shared http2.Transport. The R2 trip report
traced the ~14% transport_error on game.state at 500 players to that single
shared transport; per-player connections mirror real clients and isolate the
artifact. The assembly burst and the gateway-hammer each get their own client.

playTurn now reports when a game has finished so playerLoop drops it from the
rotation (slices.DeleteFunc); once no active game remains the player idles while
still holding its stream. This stops secondary ops from hammering game_finished
on already-ended games (the other R2 harness finding).
2026-06-10 18:53:07 +02:00

48 lines
1.4 KiB
Go

package scenario
import (
"context"
"sync"
"time"
"scrabble/loadtest/internal/edge"
"scrabble/loadtest/internal/seed"
)
// HammerConfig parameterises the gateway-hammer: how many concurrent callers and for
// how long to deliberately exceed the per-user rate limit from a single account.
type HammerConfig struct {
Workers int
Duration time.Duration
}
// DefaultHammer returns a hammer that comfortably exceeds the 300/min per-user limit.
func DefaultHammer() HammerConfig {
return HammerConfig{Workers: 20, Duration: 15 * time.Second}
}
// Hammer drives games.list from a single account far above the per-user rate limit to
// verify the limiter holds — rejections surface as the "rate_limited" code — and to
// measure its cost. Every call is recorded under "hammer:games.list" so the report
// shows the ok/rate_limited split and the rejection latency separately from the
// realistic traffic.
func (d *Driver) Hammer(ctx context.Context, acc seed.Account, cfg HammerConfig) {
runCtx, cancel := context.WithTimeout(ctx, cfg.Duration)
defer cancel()
d.log.Info("gateway-hammer", "workers", cfg.Workers, "duration", cfg.Duration)
c := edge.New(d.gateway)
var wg sync.WaitGroup
for w := 0; w < cfg.Workers; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for runCtx.Err() == nil {
t0 := time.Now()
_, code, _ := c.GamesList(runCtx, acc.Token)
d.rec.Record("hammer:games.list", code, time.Since(t0))
}
}()
}
wg.Wait()
}