R3: backend rate-limit observability — ratewatch, auto-flag, admin throttled view
- accounts.flagged_high_rate_at baked into the R1 baseline (no prod data; the contour schema is wiped after merge); jet regenerated — the regen also picks up the previously missing game_drafts/game_hidden models. - account.Store: FlagHighRate (set-once), ClearHighRateFlag, the flag in GetByID/ListUsers and a ListFlaggedHighRate review queue. - New internal/ratewatch: ingests the gateway rejection reports, keeps a bounded in-memory episode window for the console and applies the conservative auto-flag (1000 rejected / 10 min, BACKEND_HIGHRATE_FLAG_*). - POST /api/v1/internal/ratelimit/report (network-trusted, like sessions/resolve). - Admin console: Throttled page (episodes + flagged accounts), a high-rate badge in the user list, the marker + operator clear action on the user card. - Tests: ratewatch unit suite, report-route handler test, renderer cases, integration coverage for the store round-trip and the console flow.
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"scrabble/backend/internal/game"
|
||||
"scrabble/backend/internal/lobby"
|
||||
"scrabble/backend/internal/postgres"
|
||||
"scrabble/backend/internal/ratewatch"
|
||||
"scrabble/backend/internal/robot"
|
||||
"scrabble/backend/internal/telemetry"
|
||||
)
|
||||
@@ -35,6 +36,9 @@ type Config struct {
|
||||
Lobby lobby.Config
|
||||
// Robot configures the robot opponent driver (scan cadence).
|
||||
Robot robot.Config
|
||||
// RateWatch tunes the conservative high-rate auto-flag applied to the
|
||||
// gateway's rate-limiter rejection reports (R3).
|
||||
RateWatch ratewatch.Config
|
||||
// SMTP configures the email relay used for confirm-codes. An empty Host
|
||||
// selects the development log mailer (the code is logged, not sent).
|
||||
SMTP account.SMTPConfig
|
||||
@@ -105,6 +109,14 @@ func Load() (Config, error) {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
rw := ratewatch.DefaultConfig()
|
||||
if rw.FlagThreshold, err = envInt("BACKEND_HIGHRATE_FLAG_THRESHOLD", rw.FlagThreshold); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if rw.FlagWindow, err = envDuration("BACKEND_HIGHRATE_FLAG_WINDOW", rw.FlagWindow); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
guestReapInterval, err := envDuration("BACKEND_GUEST_REAP_INTERVAL", defaultGuestReapInterval)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
@@ -131,6 +143,7 @@ func Load() (Config, error) {
|
||||
Game: gm,
|
||||
Lobby: lb,
|
||||
Robot: rb,
|
||||
RateWatch: rw,
|
||||
SMTP: smtp,
|
||||
ConnectorAddr: os.Getenv("BACKEND_CONNECTOR_ADDR"),
|
||||
GuestReapInterval: guestReapInterval,
|
||||
@@ -170,6 +183,9 @@ func (c Config) validate() error {
|
||||
if err := c.Robot.Validate(); err != nil {
|
||||
return fmt.Errorf("config: %w", err)
|
||||
}
|
||||
if err := c.RateWatch.Validate(); err != nil {
|
||||
return fmt.Errorf("config: %w", err)
|
||||
}
|
||||
if c.GuestReapInterval <= 0 {
|
||||
return fmt.Errorf("config: BACKEND_GUEST_REAP_INTERVAL must be positive")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user