From 07e96dd1227a94887a853b0da870919dfb98b85c Mon Sep 17 00:00:00 2001 From: kmularise Date: Mon, 31 Jul 2023 11:05:37 +0900 Subject: [PATCH 1/3] [fix] reserve waiting queue --- .../com/gg/server/domain/match/service/MatchService.java | 7 ++++--- .../gg/server/domain/match/service/MatchServiceTest.java | 5 +++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/gg/server/domain/match/service/MatchService.java b/src/main/java/com/gg/server/domain/match/service/MatchService.java index 6a2f3eb27..bd7d98163 100644 --- a/src/main/java/com/gg/server/domain/match/service/MatchService.java +++ b/src/main/java/com/gg/server/domain/match/service/MatchService.java @@ -58,7 +58,9 @@ public synchronized void makeMatch(UserDto userDto, Option option, LocalDateTime if (enemy.isPresent()) { GameAddDto gameDto = new GameAddDto(startTime, season, player, enemy.get()); gameUpdateService.make(gameDto); - cancelEnrolledSlots(List.of(enemy.get(), player), startTime); + redisMatchTimeRepository.deleteMatchUser(startTime, player); + redisMatchTimeRepository.deleteMatchUser(startTime, enemy.get()); + cancelEnrolledSlots(List.of(enemy.get(), player)); } else { addUserToQueue(startTime, player, option); } @@ -105,8 +107,7 @@ private void addUserToQueue(LocalDateTime startTime, RedisMatchUser matchUser, O redisMatchUserRepository.addMatchTime(matchUser.getUserId(), startTime, option); } - private void cancelEnrolledSlots(List players, LocalDateTime startTime) { - redisMatchTimeRepository.deleteMatchTime(startTime); + private void cancelEnrolledSlots(List players) { for (RedisMatchUser player : players) { Set matchTimes = redisMatchUserRepository.getAllMatchTime(player.getUserId()); matchTimes.stream().forEach(ele -> redisMatchTimeRepository.deleteMatchUser(ele.getStartTime(), player)); diff --git a/src/test/java/com/gg/server/domain/match/service/MatchServiceTest.java b/src/test/java/com/gg/server/domain/match/service/MatchServiceTest.java index 142a70e0c..0ef58f8de 100644 --- a/src/test/java/com/gg/server/domain/match/service/MatchServiceTest.java +++ b/src/test/java/com/gg/server/domain/match/service/MatchServiceTest.java @@ -144,10 +144,15 @@ void makeGameWithNormalAndBoth() { @DisplayName("Queue에 매칭 가능한 normal 상대가 있을 경우 게임 생성") @Test void addMatchSameNormalOption() { + matchService.makeMatch(UserDto.from(users.get(2)), Option.RANK, this.slotTimes.get(0)); matchService.makeMatch(UserDto.from(users.get(0)), Option.NORMAL, this.slotTimes.get(0)); matchService.makeMatch(UserDto.from(users.get(1)), Option.NORMAL, this.slotTimes.get(0)); Optional game = gameRepository.findByStartTime(slotTimes.get(0)); Assertions.assertThat(game.isEmpty()).isEqualTo(false); + Long size = redisTemplate.opsForList().size(MatchKey.getTime(slotTimes.get(0))); + Assertions.assertThat(size).isEqualTo(1L); + RedisMatchUser remainedUser = (RedisMatchUser) redisTemplate.opsForList().index(MatchKey.getTime(slotTimes.get(0)), 0); + Assertions.assertThat(remainedUser.getUserId()).isEqualTo(users.get(2).getId()); } @DisplayName("Queue에 user가 선택한 random option으로 매칭 가능한 상대가 없을 경우") From 9967f51d83a7f6f60fb8cce5e8bfbc98cd5e9241 Mon Sep 17 00:00:00 2001 From: kmularise Date: Wed, 2 Aug 2023 10:39:44 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[fix]=20=EC=B7=A8=EC=86=8C=EB=8B=B9?= =?UTF-8?q?=ED=95=9C=20=EC=9C=A0=EC=A0=80=20=EB=B3=B5=EA=B7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../match/data/RedisMatchTimeRepository.java | 2 + .../match/service/GameUpdateService.java | 27 ++++---- .../domain/match/service/MatchService.java | 67 ++++++++++++++++--- 3 files changed, 73 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/gg/server/domain/match/data/RedisMatchTimeRepository.java b/src/main/java/com/gg/server/domain/match/data/RedisMatchTimeRepository.java index b396fc07e..5e4bf8dd3 100644 --- a/src/main/java/com/gg/server/domain/match/data/RedisMatchTimeRepository.java +++ b/src/main/java/com/gg/server/domain/match/data/RedisMatchTimeRepository.java @@ -1,6 +1,7 @@ package com.gg.server.domain.match.data; import com.gg.server.domain.match.type.MatchKey; +import com.gg.server.domain.user.exception.UserNotFoundException; import java.time.Duration; import java.time.LocalDateTime; import java.util.List; @@ -8,6 +9,7 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.cache.CacheProperties.Redis; import org.springframework.data.redis.core.ListOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/gg/server/domain/match/service/GameUpdateService.java b/src/main/java/com/gg/server/domain/match/service/GameUpdateService.java index d2f180123..a10c696e3 100644 --- a/src/main/java/com/gg/server/domain/match/service/GameUpdateService.java +++ b/src/main/java/com/gg/server/domain/match/service/GameUpdateService.java @@ -36,7 +36,7 @@ public class GameUpdateService { private final NotiService notiService; private final SnsNotiService snsNotiService; - public void make(GameAddDto addDto) { + public void make(GameAddDto addDto, Long recoveredUserId) { SlotManagement slotManagement = slotManagementRepository.findCurrent(LocalDateTime.now()) .orElseThrow(SlotNotFoundException::new); Game game = new Game(addDto, slotManagement.getGameInterval()); @@ -51,24 +51,25 @@ public void make(GameAddDto addDto) { TeamUser enemyTeamUser = new TeamUser(enemyTeam, enemyUser); List matchTeamUser = List.of(enemyTeamUser, myTeamUser); teamUserRepository.saveAll(matchTeamUser); - Noti playerNoti = notiService.createMatched(playerUser, addDto.getStartTime()); - snsNotiService.sendSnsNotification(playerNoti, UserDto.from(playerUser)); - Noti enemyNoti = notiService.createMatched(enemyUser, addDto.getStartTime()); - snsNotiService.sendSnsNotification(enemyNoti, UserDto.from(enemyUser)); + if (!playerUser.getId().equals(recoveredUserId)) { + Noti playerNoti = notiService.createMatched(playerUser, addDto.getStartTime()); + snsNotiService.sendSnsNotification(playerNoti, UserDto.from(playerUser)); + } + if (!enemyUser.getId().equals(recoveredUserId)) { + Noti enemyNoti = notiService.createMatched(enemyUser, addDto.getStartTime()); + snsNotiService.sendSnsNotification(enemyNoti, UserDto.from(enemyUser)); + } } - /** - * game 매칭된 user 이외에 다른 user가 취소할 경우, 에러 발생 - */ - public void delete(Game game, UserDto userDto) { - List enemyTeam = userRepository.findEnemyByGameAndUser(game.getId(), userDto.getId()); - if (enemyTeam.size() > 1) { - throw new SlotNotFoundException(); - } + public void delete(Game game, List enemyTeam) { enemyTeam.forEach(enemy -> { Noti noti = notiService.createMatchCancel(enemy, game.getStartTime()); snsNotiService.sendSnsNotification(noti, UserDto.from(enemy)); }); gameRepository.delete(game); } + + public void delete(Game game) { + gameRepository.delete(game); + } } diff --git a/src/main/java/com/gg/server/domain/match/service/MatchService.java b/src/main/java/com/gg/server/domain/match/service/MatchService.java index bd7d98163..c2f49ab56 100644 --- a/src/main/java/com/gg/server/domain/match/service/MatchService.java +++ b/src/main/java/com/gg/server/domain/match/service/MatchService.java @@ -21,11 +21,15 @@ import com.gg.server.domain.rank.redis.RedisKeyManager; import com.gg.server.domain.season.data.Season; import com.gg.server.domain.season.service.SeasonFindService; +import com.gg.server.domain.user.User; +import com.gg.server.domain.user.UserRepository; import com.gg.server.domain.user.dto.UserDto; +import com.gg.server.domain.user.exception.UserNotFoundException; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -40,6 +44,7 @@ public class MatchService { private final GameRepository gameRepository; private final PenaltyService penaltyService; private final GameUpdateService gameUpdateService; + private final UserRepository userRepository; /** * 1) 매칭 가능한 유저 있을 경우 : 게임 생성 @@ -57,10 +62,9 @@ public synchronized void makeMatch(UserDto userDto, Option option, LocalDateTime Optional enemy = matchCalculator.findEnemy(allMatchUsers); if (enemy.isPresent()) { GameAddDto gameDto = new GameAddDto(startTime, season, player, enemy.get()); - gameUpdateService.make(gameDto); - redisMatchTimeRepository.deleteMatchUser(startTime, player); - redisMatchTimeRepository.deleteMatchUser(startTime, enemy.get()); - cancelEnrolledSlots(List.of(enemy.get(), player)); + gameUpdateService.make(gameDto, -1L); + redisMatchTimeRepository.addMatchUser(startTime, player); + cancelEnrolledSlots(List.of(enemy.get(), player), startTime); } else { addUserToQueue(startTime, player, option); } @@ -68,17 +72,56 @@ public synchronized void makeMatch(UserDto userDto, Option option, LocalDateTime /** * 1) 매칭되어 게임 생성된 후 : 게임 삭제하고 알림 전송, 취소한 유저 패널티 부과 + * 복귀 유저는 매칭 가능한 상대 존재하면 다시 매칭해주고 아니면 취소 알림 보내고 큐에 등록 시킴 * 2) 매칭 전 : 큐에서 유저 삭제 * */ + /** + * game 매칭된 user 이외에 다른 user가 취소할 경우, 에러 발생 + */ @Transactional public synchronized void cancelMatch(UserDto userDto, LocalDateTime startTime) { Optional game = gameRepository.findByStartTime(startTime); if (game.isPresent()) { - gameUpdateService.delete(game.get(), userDto);//cascade 테스트 - penaltyService.givePenalty(userDto, 30); - return; + List enemyTeam = userRepository.findEnemyByGameAndUser(game.get().getId(), userDto.getId()); + if (enemyTeam.size() > 1) { + throw new SlotNotFoundException(); + } + cancelGame(userDto, startTime, game.get(), enemyTeam); + }else { + deleteUserFromQueue(userDto, startTime); + }; + } + + private void cancelGame(UserDto userDto, LocalDateTime startTime, Game game, List enemyTeam) { + /**취소한 유저 큐에서 삭제 후 패널티 부과*/ + Long recoveredUserId = enemyTeam.get(0).getId(); + List allMatchUsers = redisMatchTimeRepository.getAllMatchUsers(startTime); + RedisMatchUser penaltyUser = allMatchUsers.stream() + .filter(ele -> ele.getUserId().equals(userDto.getId())) + .findFirst() + .orElseThrow(UserNotFoundException::new); + RedisMatchUser recoveredUser = allMatchUsers.stream() + .filter(ele -> ele.getUserId().equals(recoveredUserId)) + .findFirst() + .orElseThrow(UserNotFoundException::new); + redisMatchTimeRepository.deleteMatchUser(startTime, penaltyUser); + penaltyService.givePenalty(userDto, 30); + /**취소 당한 유저 매칭 상대 찾고 있으면 다시 게임 생성 아니면 취소 알림*/ + Season season = seasonFindService.findCurrentSeason(startTime); + MatchCalculator matchCalculator = new MatchCalculator(season.getPppGap(), recoveredUser); + List targetPlayers = allMatchUsers.stream() + .filter(ele -> !ele.getUserId().equals(userDto.getId()) + && !ele.getUserId().equals(recoveredUserId)) + .collect(Collectors.toList()); + Optional enemy = matchCalculator.findEnemy(targetPlayers); + if (enemy.isPresent()) { + gameUpdateService.delete(game); + GameAddDto gameDto = new GameAddDto(startTime, season, recoveredUser, enemy.get()); + gameUpdateService.make(gameDto, recoveredUserId); + } else { + gameUpdateService.delete(game, enemyTeam); + redisMatchUserRepository.addMatchTime(recoveredUserId, startTime, recoveredUser.getOption()); } - deleteUserFromQueue(userDto, startTime); } private void checkValid(UserDto userDto, LocalDateTime startTime) { @@ -107,9 +150,13 @@ private void addUserToQueue(LocalDateTime startTime, RedisMatchUser matchUser, O redisMatchUserRepository.addMatchTime(matchUser.getUserId(), startTime, option); } - private void cancelEnrolledSlots(List players) { + private void cancelEnrolledSlots(List players, LocalDateTime targetTIme) { for (RedisMatchUser player : players) { - Set matchTimes = redisMatchUserRepository.getAllMatchTime(player.getUserId()); + Set matchTimes = redisMatchUserRepository + .getAllMatchTime(player.getUserId()) + .stream() + .filter(ele -> !ele.getStartTime().equals(targetTIme)) + .collect(Collectors.toSet()); matchTimes.stream().forEach(ele -> redisMatchTimeRepository.deleteMatchUser(ele.getStartTime(), player)); redisMatchUserRepository.deleteMatchUser(player.getUserId()); } From 054b5abf32ce102408f451d366b0bd5f3375e5e7 Mon Sep 17 00:00:00 2001 From: kmularise Date: Wed, 2 Aug 2023 10:40:44 +0900 Subject: [PATCH 3/3] [test] matching policy change --- .../match/service/MatchServiceTest.java | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/gg/server/domain/match/service/MatchServiceTest.java b/src/test/java/com/gg/server/domain/match/service/MatchServiceTest.java index 0ef58f8de..afaccf250 100644 --- a/src/test/java/com/gg/server/domain/match/service/MatchServiceTest.java +++ b/src/test/java/com/gg/server/domain/match/service/MatchServiceTest.java @@ -147,12 +147,35 @@ void addMatchSameNormalOption() { matchService.makeMatch(UserDto.from(users.get(2)), Option.RANK, this.slotTimes.get(0)); matchService.makeMatch(UserDto.from(users.get(0)), Option.NORMAL, this.slotTimes.get(0)); matchService.makeMatch(UserDto.from(users.get(1)), Option.NORMAL, this.slotTimes.get(0)); + matchService.cancelMatch(UserDto.from(users.get(0)), this.slotTimes.get(0)); Optional game = gameRepository.findByStartTime(slotTimes.get(0)); - Assertions.assertThat(game.isEmpty()).isEqualTo(false); - Long size = redisTemplate.opsForList().size(MatchKey.getTime(slotTimes.get(0))); - Assertions.assertThat(size).isEqualTo(1L); + Assertions.assertThat(game).isEmpty(); + Long size = redisTemplate.opsForList().size(MatchKey.getTime(this.slotTimes.get(0))); + List allMatchUsers = redisMatchTimeRepository.getAllMatchUsers(this.slotTimes.get(0)); + Assertions.assertThat(size).isEqualTo(2L); RedisMatchUser remainedUser = (RedisMatchUser) redisTemplate.opsForList().index(MatchKey.getTime(slotTimes.get(0)), 0); Assertions.assertThat(remainedUser.getUserId()).isEqualTo(users.get(2).getId()); + Assertions.assertThat(notiRepository.findAllByUser(users.get(1)).size()).isEqualTo(2); + Assertions.assertThat(notiRepository.findAllByUser(users.get(0)).size()).isEqualTo(1); + Assertions.assertThat(notiRepository.findAllByUser(users.get(2)).size()).isEqualTo(0); + } + + @DisplayName("게임 재생성 테스트") + @Test + void remakeGameAfterCancelling() { + matchService.makeMatch(UserDto.from(users.get(2)), Option.RANK, this.slotTimes.get(0)); + matchService.makeMatch(UserDto.from(users.get(0)), Option.NORMAL, this.slotTimes.get(0)); + matchService.makeMatch(UserDto.from(users.get(1)), Option.BOTH, this.slotTimes.get(0)); + matchService.cancelMatch(UserDto.from(users.get(2)), this.slotTimes.get(0)); + Optional game = gameRepository.findByStartTime(slotTimes.get(0)); + Assertions.assertThat(game).isPresent(); + Long size = redisTemplate.opsForList().size(MatchKey.getTime(this.slotTimes.get(0))); + Assertions.assertThat(size).isEqualTo(2L); + RedisMatchUser remainedUser = (RedisMatchUser) redisTemplate.opsForList().index(MatchKey.getTime(slotTimes.get(0)), 0); + Assertions.assertThat(remainedUser.getUserId()).isEqualTo(users.get(0).getId()); + Assertions.assertThat(notiRepository.findAllByUser(users.get(1)).size()).isEqualTo(1); + Assertions.assertThat(notiRepository.findAllByUser(users.get(0)).size()).isEqualTo(1); + Assertions.assertThat(notiRepository.findAllByUser(users.get(2)).size()).isEqualTo(1); } @DisplayName("Queue에 user가 선택한 random option으로 매칭 가능한 상대가 없을 경우")