Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.bytefight.webserver.glicko.domain.TeamStats;
import org.bytefight.webserver.glicko.infra.TeamStatsRepository;
import org.bytefight.webserver.matchmaking.domain.MatchmakingEvent;
import org.bytefight.webserver.matchmaking.infra.MatchMakingProperties;
import org.bytefight.webserver.team.application.TeamService;
import org.bytefight.webserver.team.domain.Team;
import org.springframework.stereotype.Service;
Expand All @@ -31,6 +32,7 @@ public class MatchmakingService {
private final GameMatchService gameMatchService;
private final TeamStatsRepository teamStatsRepository;
private final MatchmakingEventCreator matchmakingEventCreator;
private final MatchMakingProperties matchMakingProperties;

@Transactional
public MatchmakingEvent createAndScheduleEvent(Competition competition, String ladder) {
Expand Down Expand Up @@ -109,6 +111,20 @@ private List<GameMatch> generateMatches(
}

Collections.shuffle(edges, random);

// Bound the fan-out so one tick cannot enqueue more than the fleet can clear before the next.
// edges is already shuffled, so truncating keeps a fair random sample across teams. Unset /
// non-positive cap preserves the historical full fan-out.
Integer cap = matchMakingProperties.getMaxMatchesPerEvent();
if (cap != null && cap > 0 && edges.size() > cap) {
log.warn(
"Matchmaking fan-out for ladder '{}' capped at {} matches; dropped {} this tick",
ladder,
cap,
edges.size() - cap);
edges = edges.subList(0, cap);
}

return edges.stream()
.map(
(edge) -> {
Expand Down Expand Up @@ -146,7 +162,6 @@ public List<int[]> generate4RegularGraph(int n) {
m = Character.getNumericValue(partitionBaseCases.charAt(remaining));
} else {
m = random.nextInt(2) + 5;
System.out.println(m);
}
partitionSizes.add(m);
remaining -= m;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,12 @@ public class MatchMakingProperties {
private boolean enabled = false;
private String cron;
private String tz;

/**
* Maximum matches a single matchmaking event may enqueue. Null or non-positive means unlimited
* (the historical behaviour). Set this to bound a single cron tick to something the runner fleet
* can clear before the next tick, otherwise the backlog is monotonic. The server can't derive
* fleet size itself, so this is an ops-tuned value.
*/
private Integer maxMatchesPerEvent;
}
5 changes: 5 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ springdoc:
gamematch:
requeue-stale: true
stale-threshold-minutes: 120
matchmaking:
# max-matches-per-event bounds a single matchmaking tick. Unset = unlimited (historical
# behaviour). Set it to what the runner fleet can clear before the next tick to keep the queue
# from growing monotonically under automatic load.
max-matches-per-event:
storage:
max-decompressed-bytes: 524288000

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.bytefight.webserver.gamematch.domain.dto.GameMatchJob;
import org.bytefight.webserver.gamematch.infra.GameMatchRepository;
import org.bytefight.webserver.matchmaking.application.MatchmakingService;
import org.bytefight.webserver.matchmaking.infra.MatchMakingProperties;
import org.bytefight.webserver.storage.domain.FileRecord;
import org.bytefight.webserver.storage.infra.FileRecordRepository;
import org.bytefight.webserver.submission.domain.Submission;
Expand Down Expand Up @@ -50,6 +51,8 @@ class MatchmakingServiceIT extends FullStackIntegrationTestBase {

@Autowired private org.springframework.amqp.rabbit.connection.ConnectionFactory connectionFactory;

@Autowired private MatchMakingProperties matchMakingProperties;

@Test
void createAndScheduleEventQueuesMatches() {
String competitionSlug = "comp-mm";
Expand Down Expand Up @@ -147,6 +150,53 @@ void createAndScheduleEventWithSixTeamsQueuesTwelveMatches() {
});
}

@Test
void createAndScheduleEventRespectsMaxMatchesPerEventCap() {
Integer original = matchMakingProperties.getMaxMatchesPerEvent();
matchMakingProperties.setMaxMatchesPerEvent(5);
try {
String competitionSlug = "comp-mm-capped";
Competition competition =
testDataFactory.createCompetition(competitionSlug, "Competition", true, 2);
String ladder = "main";
testDataFactory.createLadder(competition, ladder);

for (int i = 0; i < 6; i++) {
createTeamWithSubmission(competition);
}

String routingKey = "competition." + competitionSlug + "." + ladder;
Queue scheduleQueue = QueueBuilder.nonDurable("test.matchmaking.queue.capped").build();
RabbitAdmin admin = new RabbitAdmin(connectionFactory);
admin.declareExchange(gameMatchExchange);
admin.declareQueue(scheduleQueue);
Binding binding = BindingBuilder.bind(scheduleQueue).to(gameMatchExchange).with(routingKey);
admin.declareBinding(binding);

matchmakingService.createAndScheduleEvent(competition, ladder);

int queued = 0;
Object message;
while ((message = rabbitTemplate.receiveAndConvert(scheduleQueue.getName(), 1000)) != null) {
assertThat(message).isInstanceOf(GameMatchJob.class);
queued++;
}

// Six teams would produce 12 matches uncapped; the cap truncates to exactly 5.
assertThat(queued).isEqualTo(5);

List<GameMatch> matches =
gameMatchRepository.findAll().stream()
.filter(
match -> competition.equals(ReflectionTestUtils.getField(match, "competition")))
.filter(match -> ladder.equals(ReflectionTestUtils.getField(match, "ladder")))
.toList();
assertThat(matches).hasSize(5);
} finally {
matchMakingProperties.setMaxMatchesPerEvent(original);
}
}

private Team createTeamWithSubmission(Competition competition) {
Team team = testDataFactory.createTeam(competition, UUID.randomUUID(), false);
Submission submission = createSubmission(team);
Expand Down