fix(db): eliminate O(n²) N+1 query loop in fetchBots to prevent OOMKill

The previous implementation called getBotMatchStats for each bot in a loop,
causing 10,000+ separate database queries when there are many bots. This N+1
query problem caused the pod to exceed memory limits and get OOMKilled,
resulting in CrashLoopBackOff.

Replaced with a single batch query that fetches match stats for all bots at
once, then maps the results to each bot. This reduces database round trips
from O(n) to O(1).

Fixes bead bf-2ws: acb-index-builder CrashLoopBackOff (silent crash after web asset copy)
This commit is contained in:
jedarden 2026-06-25 06:04:51 -04:00
parent c1cfcded23
commit b35a2aade0

View file

@ -334,13 +334,41 @@ func fetchBots(ctx context.Context, db *sql.DB) ([]BotData, error) {
bots = append(bots, b)
}
for i := range bots {
mp, mw, err := getBotMatchStats(ctx, db, bots[i].ID)
if err != nil {
return nil, err
// Fetch match stats for all bots in a single query to avoid O(n) query loop
// that would cause 10,000+ separate database calls (N+1 query problem)
// This fixes the CrashLoopBackOff issue caused by OOMKill during fetchBots
botMatchStats := make(map[string][2]int) // bot_id -> [matches_played, matches_won]
statsRows, err := db.QueryContext(ctx, `
SELECT mp.bot_id,
COUNT(*) as matches_played,
COUNT(*) FILTER (WHERE mp.player_slot = m.winner) as matches_won
FROM match_participants mp
JOIN matches m ON mp.match_id = m.match_id
WHERE m.status = 'completed'
GROUP BY mp.bot_id
`)
if err != nil {
return nil, fmt.Errorf("query bot match stats: %w", err)
}
defer statsRows.Close()
for statsRows.Next() {
var botID string
var played, won int
if err := statsRows.Scan(&botID, &played, &won); err != nil {
return nil, fmt.Errorf("scan bot match stats: %w", err)
}
bots[i].MatchesPlayed = mp
bots[i].MatchesWon = mw
botMatchStats[botID] = [2]int{played, won}
}
// Apply the stats to each bot
for i := range bots {
stats, ok := botMatchStats[bots[i].ID]
if ok {
bots[i].MatchesPlayed = stats[0]
bots[i].MatchesWon = stats[1]
}
// If bot has no completed matches, stats remain at 0 (already initialized)
}
return bots, nil