From b35a2aade073ed2422843ed0f7640f081249a063 Mon Sep 17 00:00:00 2001 From: jedarden Date: Thu, 25 Jun 2026 06:04:51 -0400 Subject: [PATCH] =?UTF-8?q?fix(db):=20eliminate=20O(n=C2=B2)=20N+1=20query?= =?UTF-8?q?=20loop=20in=20fetchBots=20to=20prevent=20OOMKill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- cmd/acb-index-builder/db.go | 40 +++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/cmd/acb-index-builder/db.go b/cmd/acb-index-builder/db.go index db86ccd..789dc81 100644 --- a/cmd/acb-index-builder/db.go +++ b/cmd/acb-index-builder/db.go @@ -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