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:
parent
c1cfcded23
commit
b35a2aade0
1 changed files with 34 additions and 6 deletions
|
|
@ -334,13 +334,41 @@ func fetchBots(ctx context.Context, db *sql.DB) ([]BotData, error) {
|
||||||
bots = append(bots, b)
|
bots = append(bots, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range bots {
|
// Fetch match stats for all bots in a single query to avoid O(n) query loop
|
||||||
mp, mw, err := getBotMatchStats(ctx, db, bots[i].ID)
|
// that would cause 10,000+ separate database calls (N+1 query problem)
|
||||||
if err != nil {
|
// This fixes the CrashLoopBackOff issue caused by OOMKill during fetchBots
|
||||||
return nil, err
|
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
|
botMatchStats[botID] = [2]int{played, won}
|
||||||
bots[i].MatchesWon = mw
|
}
|
||||||
|
|
||||||
|
// 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
|
return bots, nil
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue