Skip to content

Commit cf83642

Browse files
committed
Improve tournament performance
1 parent cd658a2 commit cf83642

3 files changed

Lines changed: 209 additions & 96 deletions

File tree

services/app/apps/codebattle/lib/codebattle/tournament/server.ex

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ defmodule Codebattle.Tournament.Server do
1111
require Logger
1212

1313
@type tournament_id :: pos_integer()
14+
@tournament_info_table :tournament_info_cache
1415
@waiting_room_timeout_ms to_timeout(second: 1)
1516
# API
1617
def start_link(tournament_id) do
@@ -26,7 +27,15 @@ defmodule Codebattle.Tournament.Server do
2627
end
2728

2829
def get_tournament_info(id) do
29-
GenServer.call(server_name(id), :get_tournament_info)
30+
# Try to get from ETS cache first
31+
case :ets.lookup(@tournament_info_table, id) do
32+
[{^id, tournament_info}] ->
33+
tournament_info
34+
35+
[] ->
36+
# Fall back to GenServer call if not in cache
37+
GenServer.call(server_name(id), :get_tournament_info, 20_000)
38+
end
3039
catch
3140
:exit, {:noproc, _} ->
3241
nil
@@ -37,7 +46,7 @@ defmodule Codebattle.Tournament.Server do
3746
end
3847

3948
def get_tournament(id) do
40-
GenServer.call(server_name(id), :get_tournament)
49+
GenServer.call(server_name(id), :get_tournament, 20_000)
4150
catch
4251
:exit, {:noproc, _} ->
4352
nil
@@ -60,7 +69,8 @@ defmodule Codebattle.Tournament.Server do
6069
def finish_round_after(tournament_id, round_position, timeout_in_seconds) do
6170
GenServer.call(
6271
server_name(tournament_id),
63-
{:finish_round_after, round_position, timeout_in_seconds}
72+
{:finish_round_after, round_position, timeout_in_seconds},
73+
30_000
6474
)
6575
catch
6676
:exit, reason ->
@@ -82,7 +92,7 @@ defmodule Codebattle.Tournament.Server do
8292
end
8393

8494
def handle_event(tournament_id, event_type, params) do
85-
GenServer.call(server_name(tournament_id), {:fire_event, event_type, params})
95+
GenServer.call(server_name(tournament_id), {:fire_event, event_type, params}, 20_000)
8696
catch
8797
:exit, reason ->
8898
Logger.error("Error to send tournament update: #{inspect(reason)}")
@@ -91,6 +101,11 @@ defmodule Codebattle.Tournament.Server do
91101

92102
# SERVER
93103
def init(tournament_id) do
104+
# Create tournament_info_cache table if it doesn't exist
105+
if :ets.whereis(@tournament_info_table) == :undefined do
106+
:ets.new(@tournament_info_table, [:named_table, :set, :public, read_concurrency: true])
107+
end
108+
94109
players_table = Tournament.Players.create_table(tournament_id)
95110
matches_table = Tournament.Matches.create_table(tournament_id)
96111
tasks_table = Tournament.Tasks.create_table(tournament_id)
@@ -138,6 +153,23 @@ defmodule Codebattle.Tournament.Server do
138153
end
139154

140155
def handle_call({:update, new_tournament}, _from, state) do
156+
# Update the tournament_info cache when tournament is updated
157+
tournament_info =
158+
Map.drop(new_tournament, [
159+
:__struct__,
160+
:__meta__,
161+
:creator,
162+
:event,
163+
:matches,
164+
:players,
165+
:waiting_room_state,
166+
:stats,
167+
:played_pair_ids,
168+
:round_tasks
169+
])
170+
171+
:ets.insert(@tournament_info_table, {new_tournament.id, tournament_info})
172+
141173
broadcast_tournament_update(new_tournament)
142174
{:reply, :ok, %{state | tournament: new_tournament}}
143175
end
@@ -167,19 +199,24 @@ defmodule Codebattle.Tournament.Server do
167199
end
168200

169201
def handle_call(:get_tournament_info, _from, state) do
170-
{:reply,
171-
Map.drop(state.tournament, [
172-
:__struct__,
173-
:__meta__,
174-
:creator,
175-
:event,
176-
:matches,
177-
:players,
178-
:waiting_room_state,
179-
:stats,
180-
:played_pair_ids,
181-
:round_tasks
182-
]), state}
202+
tournament_info =
203+
Map.drop(state.tournament, [
204+
:__struct__,
205+
:__meta__,
206+
:creator,
207+
:event,
208+
:matches,
209+
:players,
210+
:waiting_room_state,
211+
:stats,
212+
:played_pair_ids,
213+
:round_tasks
214+
])
215+
216+
# Update the cache
217+
:ets.insert(@tournament_info_table, {state.tournament.id, tournament_info})
218+
219+
{:reply, tournament_info, state}
183220
end
184221

185222
def handle_call({:fire_event, event_type, params}, _from, %{tournament: tournament} = state) do
@@ -192,6 +229,23 @@ defmodule Codebattle.Tournament.Server do
192229
apply(module, event_type, [tournament, params])
193230
end
194231

232+
# Update the tournament_info cache when firing events
233+
tournament_info =
234+
Map.drop(new_tournament, [
235+
:__struct__,
236+
:__meta__,
237+
:creator,
238+
:event,
239+
:matches,
240+
:players,
241+
:waiting_room_state,
242+
:stats,
243+
:played_pair_ids,
244+
:round_tasks
245+
])
246+
247+
:ets.insert(@tournament_info_table, {new_tournament.id, tournament_info})
248+
195249
# TODO: rethink broadcasting during applying event, maybe put inside tournament module
196250
broadcast_tournament_event_by_type(event_type, params, new_tournament)
197251

services/app/apps/codebattle/lib/codebattle/tournament/strategy/base.ex

Lines changed: 137 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -577,19 +577,33 @@ defmodule Codebattle.Tournament.Base do
577577
end
578578

579579
defp start_round(tournament, round_params \\ %{}) do
580+
# Perform initial updates in a single operation
581+
tournament =
582+
update_struct(tournament, %{
583+
break_state: "off",
584+
last_round_started_at: NaiveDateTime.utc_now(:second),
585+
match_timeout_seconds: Map.get(round_params, :timeout_seconds, tournament.match_timeout_seconds)
586+
})
587+
588+
# Build and save round first - this is a critical operation
589+
tournament = build_and_save_round!(tournament)
590+
591+
# Perform these operations in sequence as they depend on each other
592+
tournament =
593+
tournament
594+
|> maybe_preload_tasks()
595+
|> maybe_set_round_task_ids()
596+
|> maybe_start_round_timer()
597+
|> maybe_activate_players()
598+
599+
# Build matches - this is the most time-consuming part
600+
tournament = build_round_matches(tournament, round_params)
601+
602+
# Save to database
603+
tournament = db_save!(tournament)
604+
605+
# These operations can be done after the critical path
580606
tournament
581-
|> update_struct(%{
582-
break_state: "off",
583-
last_round_started_at: NaiveDateTime.utc_now(:second),
584-
match_timeout_seconds: Map.get(round_params, :timeout_seconds, tournament.match_timeout_seconds)
585-
})
586-
|> build_and_save_round!()
587-
|> maybe_preload_tasks()
588-
|> maybe_set_round_task_ids()
589-
|> maybe_start_round_timer()
590-
|> maybe_activate_players()
591-
|> build_round_matches(round_params)
592-
|> db_save!()
593607
|> maybe_start_waiting_room()
594608
|> broadcast_round_created()
595609
end
@@ -648,40 +662,51 @@ defmodule Codebattle.Tournament.Base do
648662
defp bulk_create_round_games_and_matches(batch, tournament, task, timeout_seconds) do
649663
reset_task_ids = tournament.task_provider == "task_pack_per_round"
650664

651-
batch
652-
|> Enum.map(fn
653-
# TODO: skip bots game
654-
# {[p1 = %{is_bot: true}, p2 = %{is_bot: true}], match_id} ->
655-
# Tournament.Matches.put_match(tournament, %Tournament.Match{
656-
# id: match_id,
657-
# state: "canceled",
658-
# round_id: tournament.current_round_id,
659-
# round_position: tournament.current_round_position,
660-
# player_ids: Enum.sort([p1.id, p2.id])
661-
# })
662-
663-
{[p1, p2] = players, match_id} ->
664-
%{
665-
players: players,
666-
ref: match_id,
667-
round_id: tournament.current_round_id,
668-
state: "playing",
669-
task: task,
670-
waiting_room_name: tournament.waiting_room_name,
671-
timeout_seconds: timeout_seconds,
672-
tournament_id: tournament.id,
673-
type: game_type(),
674-
use_chat: tournament.use_chat,
675-
use_timer: tournament.use_timer
676-
}
677-
|> maybe_set_free_task(tournament, p1)
678-
|> maybe_add_award(tournament)
679-
end)
680-
|> Game.Context.bulk_create_games()
681-
|> Enum.zip(batch)
682-
|> Enum.each(fn {game, {players, _match_id}} ->
683-
build_and_run_match(tournament, players, game, reset_task_ids)
684-
end)
665+
# Prepare game creation parameters in a single pass
666+
game_params =
667+
Enum.map(batch, fn
668+
{[p1, p2] = players, match_id} ->
669+
base_params = %{
670+
players: players,
671+
ref: match_id,
672+
round_id: tournament.current_round_id,
673+
state: "playing",
674+
task: task,
675+
waiting_room_name: tournament.waiting_room_name,
676+
timeout_seconds: timeout_seconds,
677+
tournament_id: tournament.id,
678+
type: game_type(),
679+
use_chat: tournament.use_chat,
680+
use_timer: tournament.use_timer
681+
}
682+
683+
# Apply transformations
684+
params =
685+
base_params
686+
|> maybe_set_free_task(tournament, p1)
687+
|> maybe_add_award(tournament)
688+
689+
{params, players, match_id}
690+
end)
691+
692+
# Extract just the game parameters for bulk creation
693+
game_creation_params = Enum.map(game_params, fn {params, _players, _match_id} -> params end)
694+
695+
# Create games in bulk
696+
created_games = Game.Context.bulk_create_games(game_creation_params)
697+
698+
# Process matches in parallel using Task.async_stream with controlled concurrency
699+
created_games
700+
|> Enum.zip(game_params)
701+
|> Task.async_stream(
702+
fn {game, {_params, players, _match_id}} ->
703+
build_and_run_match(tournament, players, game, reset_task_ids)
704+
end,
705+
max_concurrency: System.schedulers_online(),
706+
ordered: false,
707+
timeout: 30_000
708+
)
709+
|> Stream.run()
685710
end
686711

687712
defp create_rematch_game(tournament, players, ref) do
@@ -1026,43 +1051,77 @@ defmodule Codebattle.Tournament.Base do
10261051
matches_to_finish = get_matches(tournament, "playing")
10271052
finished_at = DateTime.utc_now(:second)
10281053

1029-
Enum.each(
1030-
matches_to_finish,
1031-
fn match ->
1032-
duration_sec = NaiveDateTime.diff(finished_at, match.started_at)
1033-
1034-
player_results = improve_player_results(tournament, match, duration_sec)
1035-
Game.Context.trigger_timeout(match.game_id)
1054+
# Early return if no matches to finish
1055+
if matches_to_finish == [] do
1056+
tournament
1057+
else
1058+
# Process matches in parallel with Task.async_stream
1059+
match_results =
1060+
matches_to_finish
1061+
|> Task.async_stream(
1062+
fn match ->
1063+
duration_sec = NaiveDateTime.diff(finished_at, match.started_at)
1064+
1065+
# Get player results and trigger timeout
1066+
player_results = improve_player_results(tournament, match, duration_sec)
1067+
Game.Context.trigger_timeout(match.game_id)
1068+
1069+
# Create new match with timeout state
1070+
new_match = %{
1071+
match
1072+
| state: "timeout",
1073+
player_results: player_results,
1074+
duration_sec: duration_sec,
1075+
finished_at: finished_at
1076+
}
1077+
1078+
# Return match and player data for batch processing
1079+
{new_match, player_results}
1080+
end,
1081+
max_concurrency: System.schedulers_online() * 2,
1082+
timeout: 10_000
1083+
)
1084+
|> Enum.to_list()
10361085

1037-
new_match = %{
1038-
match
1039-
| state: "timeout",
1040-
player_results: player_results,
1041-
duration_sec: duration_sec,
1042-
finished_at: finished_at
1043-
}
1086+
# Batch update matches and collect player updates
1087+
player_updates =
1088+
Enum.reduce(match_results, %{}, fn {:ok, {new_match, player_results}}, acc ->
1089+
# Update match in tournament
1090+
Tournament.Matches.put_match(tournament, new_match)
10441091

1045-
Tournament.Matches.put_match(tournament, new_match)
1092+
# Broadcast match update
1093+
Codebattle.PubSub.broadcast("tournament:match:upserted", %{
1094+
tournament: tournament,
1095+
match: new_match
1096+
})
10461097

1047-
Codebattle.PubSub.broadcast("tournament:match:upserted", %{
1048-
tournament: tournament,
1049-
match: new_match
1050-
})
1098+
# Collect player updates
1099+
Enum.reduce(player_results, acc, fn {player_id, result}, player_acc ->
1100+
player_score = result.score
1101+
player_lang = result.lang
10511102

1052-
player_results
1053-
|> Map.keys()
1054-
|> Enum.each(fn player_id ->
1055-
player = Tournament.Players.get_player(tournament, player_id)
1056-
1057-
player &&
1058-
Tournament.Players.put_player(tournament, %{
1059-
player
1060-
| score: player.score + player_results[player_id].score,
1061-
lang: player_results[player_id].lang
1062-
})
1103+
# credo:disable-for-next-line Credo.Check.Refactor.Nesting
1104+
Map.update(player_acc, player_id, %{score: player_score, lang: player_lang}, fn existing ->
1105+
%{score: existing.score + player_score, lang: player_lang}
1106+
end)
1107+
end)
10631108
end)
1064-
end
1065-
)
1109+
1110+
# Batch update player scores
1111+
Enum.each(player_updates, fn {player_id, updates} ->
1112+
player = Tournament.Players.get_player(tournament, player_id)
1113+
1114+
if player do
1115+
Tournament.Players.put_player(tournament, %{
1116+
player
1117+
| score: player.score + updates.score,
1118+
lang: updates.lang
1119+
})
1120+
end
1121+
end)
1122+
1123+
tournament
1124+
end
10661125
end
10671126

10681127
defp improve_player_results(tournament, match, duration_sec) do

0 commit comments

Comments
 (0)