Skip to content

Commit 12e6edb

Browse files
committed
fix: solve atomic conflict
1 parent 08ac810 commit 12e6edb

4 files changed

Lines changed: 48 additions & 16 deletions

File tree

app/modules/ai_intelligence/jobs/rebuild_champion_matrix_job.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,21 @@ class RebuildChampionMatrixJob < ApplicationJob
88
queue_as :low_priority
99

1010
def perform(scope: :all, league: nil)
11-
lock_key = "sidekiq:rebuild_champion_matrix:lock"
12-
acquired = Sidekiq.redis { |r| r.call("SET", lock_key, "1", "NX", "EX", 3600) }
11+
lock_key = 'sidekiq:rebuild_champion_matrix:lock'
12+
acquired = Sidekiq.redis { |r| r.call('SET', lock_key, '1', 'NX', 'EX', 3600) }
1313

1414
unless acquired
15-
Rails.logger.info("[AI] RebuildChampionMatrixJob skipped — already running")
15+
Rails.logger.info('[AI] RebuildChampionMatrixJob skipped — already running')
1616
return
1717
end
1818

19+
# 31k+ records with per-row upserts exceed the default 10s statement_timeout.
20+
# Scope this to the current session only — the connection returns to the pool
21+
# with its normal timeout restored after the job finishes.
22+
ActiveRecord::Base.connection.execute('SET LOCAL statement_timeout = 0')
1923
rebuild_matrices(scope:, league:)
2024
ensure
21-
Sidekiq.redis { |r| r.call("DEL", lock_key) } if acquired
25+
Sidekiq.redis { |r| r.call('DEL', lock_key) } if acquired
2226
end
2327

2428
private

app/modules/ai_intelligence/models/ai_champion_matrix.rb

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,24 @@ class AiChampionMatrix < ApplicationRecord
99
scope :with_sufficient_sample, -> { where('total_games >= ?', 10) }
1010

1111
def self.upsert_win(winner, loser, patch: nil, league: nil)
12-
matrix = find_or_initialize_by(champion_a: winner, champion_b: loser, patch: patch, league: league)
13-
matrix.wins_a = matrix.wins_a.to_i + 1
14-
matrix.total_games = matrix.total_games.to_i + 1
15-
matrix.updated_at = Time.current
16-
matrix.save!
12+
# Two separate partial indexes cover the two cases:
13+
# - both null → index_ai_champion_matrices_null_pair
14+
# - both present → index_ai_champion_matrices_unique
15+
index = if patch.nil? && league.nil?
16+
:index_ai_champion_matrices_null_pair
17+
else
18+
:index_ai_champion_matrices_unique
19+
end
20+
upsert(
21+
{ champion_a: winner, champion_b: loser, patch: patch, league: league,
22+
wins_a: 1, total_games: 1, updated_at: Time.current },
23+
unique_by: index,
24+
on_duplicate: Arel.sql(
25+
'wins_a = ai_champion_matrices.wins_a + 1, ' \
26+
'total_games = ai_champion_matrices.total_games + 1, ' \
27+
'updated_at = excluded.updated_at'
28+
)
29+
)
1730
end
1831

1932
def win_rate

app/modules/ai_intelligence/services/champion_matrix_builder.rb

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,14 @@ def register_matchups(winner_picks, loser_picks)
4343
end
4444

4545
def record_appearance(champion_a, champion_b)
46-
AiChampionMatrix
47-
.find_or_initialize_by(champion_a:, champion_b:)
48-
.tap do |m|
49-
m.total_games = m.total_games.to_i + 1
50-
m.updated_at = Time.current
51-
m.save!
52-
end
46+
AiChampionMatrix.upsert(
47+
{ champion_a: champion_a, champion_b: champion_b, patch: nil, league: nil,
48+
wins_a: 0, total_games: 1, updated_at: Time.current },
49+
unique_by: :index_ai_champion_matrices_null_pair,
50+
on_duplicate: Arel.sql(
51+
'total_games = ai_champion_matrices.total_games + 1, ' \
52+
'updated_at = excluded.updated_at'
53+
)
54+
)
5355
end
5456
end
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# frozen_string_literal: true
2+
3+
class AddNullPairIndexToAiChampionMatrices < ActiveRecord::Migration[7.1]
4+
def change
5+
# Partial index covering rows where both patch and league are NULL.
6+
# The existing index_ai_champion_matrices_unique only covers non-null patch+league.
7+
# Without this, upsert with ON CONFLICT cannot target the null-patch/league rows.
8+
add_index :ai_champion_matrices, %i[champion_a champion_b],
9+
name: 'index_ai_champion_matrices_null_pair',
10+
unique: true,
11+
where: 'patch IS NULL AND league IS NULL'
12+
end
13+
end

0 commit comments

Comments
 (0)