diff --git a/src/main/java/hyundai/softeer/orange/comment/scheduler/CommentScheduler.java b/src/main/java/hyundai/softeer/orange/comment/scheduler/CommentScheduler.java index 8ea91f6c..3bc91e17 100644 --- a/src/main/java/hyundai/softeer/orange/comment/scheduler/CommentScheduler.java +++ b/src/main/java/hyundai/softeer/orange/comment/scheduler/CommentScheduler.java @@ -4,22 +4,39 @@ import hyundai.softeer.orange.common.util.ConstantUtil; import hyundai.softeer.orange.event.common.repository.EventFrameRepository; import lombok.RequiredArgsConstructor; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.List; +import java.util.concurrent.TimeUnit; @RequiredArgsConstructor @Component public class CommentScheduler { + // 분산 환경에서 메서드가 여러 번 실행되는 것을 방지하기 위해 분산 락 도입 + private final RedissonClient redissonClient; private final CommentService commentService; private final EventFrameRepository eventFrameRepository; // 스케줄러에 의해 일정 시간마다 캐싱된 긍정 기대평 목록을 초기화한다. @Scheduled(fixedRate = ConstantUtil.SCHEDULED_TIME) // 2시간마다 실행 - private void clearCache() { - List frameIds = eventFrameRepository.findAllFrameIds(); - frameIds.forEach(commentService::getComments); + private void clearCache() throws InterruptedException { + RLock lock = redissonClient.getLock(ConstantUtil.DB_TO_REDIS_LOCK); + try { + // 5분동안 락 점유 + if (lock.tryLock(0, 5, TimeUnit.MINUTES)) { + try { + List frameIds = eventFrameRepository.findAllFrameIds(); + frameIds.forEach(commentService::getComments); + } finally { + lock.unlock(); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } } } diff --git a/src/main/java/hyundai/softeer/orange/common/util/ConstantUtil.java b/src/main/java/hyundai/softeer/orange/common/util/ConstantUtil.java index 86a1341e..b7ae2240 100644 --- a/src/main/java/hyundai/softeer/orange/common/util/ConstantUtil.java +++ b/src/main/java/hyundai/softeer/orange/common/util/ConstantUtil.java @@ -22,6 +22,9 @@ public class ConstantUtil { public static final String WAITING = "waiting"; public static final String PROGRESS = "progress"; + public static final String DB_TO_REDIS_LOCK = "FCFS_MANAGE_DB_TO_REDIS"; + public static final String REDIS_TO_DB_LOCK = "FCFS_MANAGE_REDIS_TO_DB"; + public static final double LIMIT_NEGATIVE_CONFIDENCE = 99.5; public static final int COMMENTS_SIZE = 20; public static final int SCHEDULED_TIME = 1000 * 60 * 60 * 2; diff --git a/src/main/java/hyundai/softeer/orange/event/fcfs/controller/FcfsController.java b/src/main/java/hyundai/softeer/orange/event/fcfs/controller/FcfsController.java index 41a67099..7b28f056 100644 --- a/src/main/java/hyundai/softeer/orange/event/fcfs/controller/FcfsController.java +++ b/src/main/java/hyundai/softeer/orange/event/fcfs/controller/FcfsController.java @@ -33,39 +33,39 @@ public class FcfsController { private final FcfsManageService fcfsManageService; @EventUserAuthRequirement @Auth(AuthRole.event_user) - @PostMapping("/{eventSequence}") + @PostMapping("/{eventId}") @Operation(summary = "선착순 이벤트 참여", description = "선착순 이벤트에 참여한 결과(boolean)를 반환한다.", responses = { @ApiResponse(responseCode = "200", description = "선착순 이벤트 당첨 성공 혹은 실패", content = @Content(schema = @Schema(implementation = ResponseFcfsResultDto.class))), @ApiResponse(responseCode = "400", description = "선착순 이벤트 시간이 아니거나, 요청 형식이 잘못된 경우", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) - public ResponseEntity participate(@Parameter(hidden = true) @EventUserAnnotation EventUserInfo userInfo, @PathVariable Long eventSequence, @RequestBody RequestAnswerDto dto) { - boolean answerResult = fcfsAnswerService.judgeAnswer(eventSequence, dto.getAnswer()); - boolean isWin = answerResult && fcfsService.participate(eventSequence, userInfo.getUserId()); + public ResponseEntity participate(@Parameter(hidden = true) @EventUserAnnotation EventUserInfo userInfo, @PathVariable String eventId, @RequestBody RequestAnswerDto dto) { + boolean answerResult = fcfsAnswerService.judgeAnswer(eventId, dto.getAnswer()); + boolean isWin = answerResult && fcfsService.participate(eventId, userInfo.getUserId()); return ResponseEntity.ok(new ResponseFcfsResultDto(answerResult, isWin)); } - @GetMapping("/{eventSequence}/info") + @GetMapping("/{eventId}/info") @Operation(summary = "특정 선착순 이벤트의 정보 조회", description = "특정 선착순 이벤트에 대한 정보(서버 기준 시각, 이벤트의 상태)를 반환한다.", responses = { @ApiResponse(responseCode = "200", description = "선착순 이벤트에 대한 상태 정보", content = @Content(schema = @Schema(implementation = ResponseFcfsInfoDto.class))), @ApiResponse(responseCode = "404", description = "선착순 이벤트를 찾을 수 없는 경우", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) - public ResponseEntity getFcfsInfo(@PathVariable Long eventSequence) { - return ResponseEntity.ok(fcfsManageService.getFcfsInfo(eventSequence)); + public ResponseEntity getFcfsInfo(@PathVariable String eventId) { + return ResponseEntity.ok(fcfsManageService.getFcfsInfo(eventId)); } @EventUserAuthRequirement @Auth(AuthRole.event_user) - @GetMapping("/{eventSequence}/participated") + @GetMapping("/{eventId}/participated") @Operation(summary = "선착순 이벤트 참여 여부 조회", description = "정답을 맞혀서 선착순 이벤트에 참여했는지 여부를 조회한다. (당첨은 별도)", responses = { @ApiResponse(responseCode = "200", description = "선착순 이벤트의 정답을 맞혀서 참여했는지에 대한 결과", content = @Content(schema = @Schema(implementation = ResponseFcfsResultDto.class))), @ApiResponse(responseCode = "404", description = "선착순 이벤트를 찾을 수 없는 경우", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) - public ResponseEntity isParticipated(@Parameter(hidden = true) @EventUserAnnotation EventUserInfo userInfo, @PathVariable Long eventSequence) { - return ResponseEntity.ok(fcfsManageService.isParticipated(eventSequence, userInfo.getUserId())); + public ResponseEntity isParticipated(@Parameter(hidden = true) @EventUserAnnotation EventUserInfo userInfo, @PathVariable String eventId) { + return ResponseEntity.ok(fcfsManageService.isParticipated(eventId, userInfo.getUserId())); } } diff --git a/src/main/java/hyundai/softeer/orange/event/fcfs/scheduler/FcfsScheduler.java b/src/main/java/hyundai/softeer/orange/event/fcfs/scheduler/FcfsScheduler.java index 07a3d041..646b0f30 100644 --- a/src/main/java/hyundai/softeer/orange/event/fcfs/scheduler/FcfsScheduler.java +++ b/src/main/java/hyundai/softeer/orange/event/fcfs/scheduler/FcfsScheduler.java @@ -1,27 +1,58 @@ package hyundai.softeer.orange.event.fcfs.scheduler; +import hyundai.softeer.orange.common.util.ConstantUtil; import hyundai.softeer.orange.event.fcfs.service.FcfsManageService; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; +import java.util.concurrent.TimeUnit; + @RequiredArgsConstructor @Component public class FcfsScheduler { + // 분산 환경에서 메서드가 여러 번 실행되는 것을 방지하기 위해 분산 락 도입 + private final RedissonClient redissonClient; private final FcfsManageService fcfsManageService; // 매일 자정 1분마다 실행되며, 오늘의 선착순 이벤트에 대한 정보를 DB에서 Redis로 이동시킨다. @Scheduled(cron = "0 1 0 * * *") public void registerFcfsEvents() { - fcfsManageService.registerFcfsEvents(); + RLock lock = redissonClient.getLock(ConstantUtil.DB_TO_REDIS_LOCK); + try { + // 5분동안 락 점유 + if (lock.tryLock(0, 5, TimeUnit.MINUTES)) { + try { + fcfsManageService.registerFcfsEvents(); + } finally { + lock.unlock(); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } } // 매일 자정마다 실행되며, 선착순 이벤트 당첨자들을 Redis에서 DB로 이동시킨다. @Scheduled(cron = "0 0 0 * * *") public void registerWinners() { - fcfsManageService.registerWinners(); + RLock lock = redissonClient.getLock(ConstantUtil.REDIS_TO_DB_LOCK); + try { + // 5분동안 락 점유 + if (lock.tryLock(0, 5, TimeUnit.MINUTES)) { + try { + fcfsManageService.registerWinners(); + } finally { + lock.unlock(); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } } // FIXME: 빌드 직후 오늘의 선착순 이벤트에 대한 정보를 DB에서 Redis로 이동시킨다. (추후 삭제예정) diff --git a/src/main/java/hyundai/softeer/orange/event/fcfs/service/DbFcfsService.java b/src/main/java/hyundai/softeer/orange/event/fcfs/service/DbFcfsService.java index 261c5715..866c7772 100644 --- a/src/main/java/hyundai/softeer/orange/event/fcfs/service/DbFcfsService.java +++ b/src/main/java/hyundai/softeer/orange/event/fcfs/service/DbFcfsService.java @@ -12,6 +12,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -26,11 +27,17 @@ public class DbFcfsService implements FcfsService{ private final EventUserRepository eventUserRepository; private final FcfsEventWinningInfoRepository fcfsEventWinningInfoRepository; private final RedisTemplate booleanRedisTemplate; + private final StringRedisTemplate stringRedisTemplate; @Override @Transactional - public boolean participate(Long eventSequence, String userId){ - String key = eventSequence.toString(); + public boolean participate(String eventId, String userId){ + String key = stringRedisTemplate.opsForValue().get(FcfsUtil.eventIdFormatting(eventId)); + if(key == null) { + throw new FcfsEventException(ErrorCode.EVENT_NOT_FOUND); + } + Long eventSequence = Long.parseLong(key); + // 이벤트 종료 여부 확인 if (isEventEnded(key)) { return false; diff --git a/src/main/java/hyundai/softeer/orange/event/fcfs/service/FcfsAnswerService.java b/src/main/java/hyundai/softeer/orange/event/fcfs/service/FcfsAnswerService.java index 79c1ce6b..a859db6d 100644 --- a/src/main/java/hyundai/softeer/orange/event/fcfs/service/FcfsAnswerService.java +++ b/src/main/java/hyundai/softeer/orange/event/fcfs/service/FcfsAnswerService.java @@ -15,9 +15,15 @@ public class FcfsAnswerService { private final StringRedisTemplate stringRedisTemplate; - public boolean judgeAnswer(Long eventSequence, String answer) { + public boolean judgeAnswer(String eventId, String answer) { + // eventId로부터 FCFS의 key를 가져옴 + String key = stringRedisTemplate.opsForValue().get(FcfsUtil.eventIdFormatting(eventId)); + if(key == null) { + throw new FcfsEventException(ErrorCode.FCFS_EVENT_NOT_FOUND); + } + // 잘못된 이벤트 참여 시간 - String startTime = stringRedisTemplate.opsForValue().get(FcfsUtil.startTimeFormatting(eventSequence.toString())); + String startTime = stringRedisTemplate.opsForValue().get(FcfsUtil.startTimeFormatting(key)); if(startTime == null) { throw new FcfsEventException(ErrorCode.FCFS_EVENT_NOT_FOUND); } @@ -26,7 +32,7 @@ public boolean judgeAnswer(Long eventSequence, String answer) { } // 정답 비교 - String correctAnswer = stringRedisTemplate.opsForValue().get(FcfsUtil.answerFormatting(eventSequence.toString())); + String correctAnswer = stringRedisTemplate.opsForValue().get(FcfsUtil.answerFormatting(key)); if (correctAnswer == null) { throw new FcfsEventException(ErrorCode.FCFS_EVENT_NOT_FOUND); } diff --git a/src/main/java/hyundai/softeer/orange/event/fcfs/service/FcfsManageService.java b/src/main/java/hyundai/softeer/orange/event/fcfs/service/FcfsManageService.java index 1b1a91d0..dc926ad3 100644 --- a/src/main/java/hyundai/softeer/orange/event/fcfs/service/FcfsManageService.java +++ b/src/main/java/hyundai/softeer/orange/event/fcfs/service/FcfsManageService.java @@ -74,12 +74,21 @@ public void registerWinners() { fcfsEventWinningInfoRepository.saveAll(winningInfos); deleteEventInfo(eventId); } + + Set eventIds = stringRedisTemplate.keys("*:eventId"); + if(eventIds != null && !eventIds.isEmpty()) { + for(String eventId : eventIds) { + stringRedisTemplate.delete(eventId); + } + } log.info("Winners of all FCFS events were registered in DB"); } // 특정 선착순 이벤트의 정보 조회 - public ResponseFcfsInfoDto getFcfsInfo(Long eventSequence) { - String startTime = stringRedisTemplate.opsForValue().get(FcfsUtil.startTimeFormatting(eventSequence.toString())); + public ResponseFcfsInfoDto getFcfsInfo(String eventId) { + String key = getFcfsKeyFromEventId(eventId); + + String startTime = stringRedisTemplate.opsForValue().get(FcfsUtil.startTimeFormatting(key)); // 선착순 이벤트가 존재하지 않는 경우 if (startTime == null) { throw new FcfsEventException(ErrorCode.FCFS_EVENT_NOT_FOUND); @@ -91,7 +100,7 @@ public ResponseFcfsInfoDto getFcfsInfo(Long eventSequence) { // 서버시간 < 이벤트시작시간 < 서버시간+3시간 -> countdown // 이벤트시작시간 < 서버시간 < 이벤트시작시간+7시간 -> progress // 그 외 -> waiting - log.info("Checked FCFS event status: {}", eventSequence); + log.info("Checked FCFS event status: {}", key); if(nowDateTime.isBefore(eventStartTime) && nowDateTime.plusHours(ConstantUtil.FCFS_COUNTDOWN_HOUR).isAfter(eventStartTime)) { return new ResponseFcfsInfoDto(eventStartTime, ConstantUtil.COUNTDOWN); } else if(eventStartTime.isBefore(nowDateTime) && eventStartTime.plusHours(ConstantUtil.FCFS_AVAILABLE_HOUR).isAfter(nowDateTime)) { @@ -103,11 +112,12 @@ public ResponseFcfsInfoDto getFcfsInfo(Long eventSequence) { // 특정 유저가 선착순 이벤트의 참여자인지 조회 (정답을 맞힌 경우 참여자로 간주) @Transactional(readOnly = true) - public Boolean isParticipated(Long eventSequence, String userId) { - if(!fcfsEventRepository.existsById(eventSequence)) { + public Boolean isParticipated(String eventId, String userId) { + String key = getFcfsKeyFromEventId(eventId); + if(!fcfsEventRepository.existsById(Long.parseLong(key))) { throw new FcfsEventException(ErrorCode.FCFS_EVENT_NOT_FOUND); } - return Boolean.TRUE.equals(stringRedisTemplate.opsForSet().isMember(FcfsUtil.participantFormatting(eventSequence.toString()), userId)); + return Boolean.TRUE.equals(stringRedisTemplate.opsForSet().isMember(FcfsUtil.participantFormatting(key), userId)); } // 특정 선착순 이벤트의 당첨자 조회 - 어드민에서 사용 @@ -126,6 +136,7 @@ public List getFcfsWinnersInfo(Long eventSequence) { private void prepareEventInfo(FcfsEvent event) { String key = event.getId().toString(); + stringRedisTemplate.opsForValue().set(FcfsUtil.eventIdFormatting(event.getEventMetaData().getEventId()), key); numberRedisTemplate.opsForValue().set(FcfsUtil.keyFormatting(key), event.getParticipantCount().intValue()); booleanRedisTemplate.opsForValue().set(FcfsUtil.endFlagFormatting(key), false); stringRedisTemplate.opsForValue().set(FcfsUtil.startTimeFormatting(key), event.getStartTime().toString()); @@ -154,4 +165,12 @@ private LocalDateTime getTimeFromScore(Double score) { long timeMillis = score.longValue(); return LocalDateTime.ofInstant(Instant.ofEpochMilli(timeMillis), ZoneId.systemDefault()); } + + private String getFcfsKeyFromEventId(String eventId) { + String key = stringRedisTemplate.opsForValue().get(eventId); + if(key == null) { + throw new FcfsEventException(ErrorCode.FCFS_EVENT_NOT_FOUND); + } + return key; + } } diff --git a/src/main/java/hyundai/softeer/orange/event/fcfs/service/FcfsService.java b/src/main/java/hyundai/softeer/orange/event/fcfs/service/FcfsService.java index 73a2989c..56cc428d 100644 --- a/src/main/java/hyundai/softeer/orange/event/fcfs/service/FcfsService.java +++ b/src/main/java/hyundai/softeer/orange/event/fcfs/service/FcfsService.java @@ -1,6 +1,6 @@ package hyundai.softeer.orange.event.fcfs.service; public interface FcfsService { - boolean participate(Long eventSequence, String userId); + boolean participate(String eventId, String userId); } diff --git a/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisLockFcfsService.java b/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisLockFcfsService.java index 78438505..16c2db99 100644 --- a/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisLockFcfsService.java +++ b/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisLockFcfsService.java @@ -25,8 +25,13 @@ public class RedisLockFcfsService implements FcfsService { private final RedisTemplate numberRedisTemplate; @Override - public boolean participate(Long eventSequence, String userId) { - String key = eventSequence.toString(); + public boolean participate(String eventId, String userId) { + String key = stringRedisTemplate.opsForValue().get(FcfsUtil.eventIdFormatting(eventId)); + if(key == null) { + throw new FcfsEventException(ErrorCode.EVENT_NOT_FOUND); + } + Long eventSequence = Long.parseLong(key); + // 이벤트 종료 여부 확인 if (isEventEnded(key)) { stringRedisTemplate.opsForSet().add(FcfsUtil.participantFormatting(key), userId); diff --git a/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisLuaFcfsService.java b/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisLuaFcfsService.java index 7ed0a024..7faf680c 100644 --- a/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisLuaFcfsService.java +++ b/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisLuaFcfsService.java @@ -26,8 +26,13 @@ public class RedisLuaFcfsService implements FcfsService { private final RedisTemplate booleanRedisTemplate; @Override - public boolean participate(Long eventSequence, String userId) { - String key = eventSequence.toString(); + public boolean participate(String eventId, String userId) { + String key = stringRedisTemplate.opsForValue().get(FcfsUtil.eventIdFormatting(eventId)); + if(key == null) { + throw new FcfsEventException(ErrorCode.EVENT_NOT_FOUND); + } + Long eventSequence = Long.parseLong(key); + // 이벤트 종료 여부 확인 if (isEventEnded(key)) { stringRedisTemplate.opsForSet().add(FcfsUtil.participantFormatting(key), userId); diff --git a/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisSetFcfsService.java b/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisSetFcfsService.java index 692b2185..19509da5 100644 --- a/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisSetFcfsService.java +++ b/src/main/java/hyundai/softeer/orange/event/fcfs/service/RedisSetFcfsService.java @@ -21,8 +21,13 @@ public class RedisSetFcfsService implements FcfsService { private final StringRedisTemplate stringRedisTemplate; @Override - public boolean participate(Long eventSequence, String userId) { - String key = eventSequence.toString(); + public boolean participate(String eventId, String userId) { + String key = stringRedisTemplate.opsForValue().get(FcfsUtil.eventIdFormatting(eventId)); + if(key == null) { + throw new FcfsEventException(ErrorCode.EVENT_NOT_FOUND); + } + Long eventSequence = Long.parseLong(key); + // 이벤트 종료 여부 확인 if(isEventEnded(key)) { stringRedisTemplate.opsForSet().add(FcfsUtil.participantFormatting(key), userId); diff --git a/src/main/java/hyundai/softeer/orange/event/fcfs/util/FcfsUtil.java b/src/main/java/hyundai/softeer/orange/event/fcfs/util/FcfsUtil.java index dc52e7cf..ff32424c 100644 --- a/src/main/java/hyundai/softeer/orange/event/fcfs/util/FcfsUtil.java +++ b/src/main/java/hyundai/softeer/orange/event/fcfs/util/FcfsUtil.java @@ -6,6 +6,11 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class FcfsUtil { + // 선착순 이벤트의 PK를 간접적으로 보관하는 eventId tag + public static String eventIdFormatting(String key) { + return key + ":eventId"; + } + // 선착순 이벤트 tag public static String keyFormatting(String key) { return key + ":fcfs"; @@ -13,22 +18,22 @@ public static String keyFormatting(String key) { // 선착순 이벤트 시작 시각 tag public static String startTimeFormatting(String key) { - return key + "_start"; + return key + ":start"; } // 선착순 이벤트 마감 여부 tag public static String endFlagFormatting(String key) { - return key + "_end"; + return key + ":end"; } // 선착순 이벤트 당첨자 tag public static String winnerFormatting(String key) { - return key + "_winner"; + return key + ":winner"; } // 선착순 이벤트 참여자 tag public static String participantFormatting(String key) { - return key + "_participant"; + return key + ":participant"; } // 선착순 이벤트 정답 tag diff --git a/src/test/java/hyundai/softeer/orange/event/fcfs/FcfsControllerTest.java b/src/test/java/hyundai/softeer/orange/event/fcfs/FcfsControllerTest.java index 72050e43..d6eab2b3 100644 --- a/src/test/java/hyundai/softeer/orange/event/fcfs/FcfsControllerTest.java +++ b/src/test/java/hyundai/softeer/orange/event/fcfs/FcfsControllerTest.java @@ -61,7 +61,7 @@ class FcfsControllerTest { String userId = "testUserId"; String answer = "answer"; - Long eventSequence = 1L; + String eventId = "HD_240808_001"; @BeforeEach void setUp() throws Exception { @@ -78,18 +78,18 @@ void setUp() throws Exception { void participateTest(boolean isWinner) throws Exception { // given ResponseFcfsResultDto responseFcfsResultDto = new ResponseFcfsResultDto(true, isWinner); - when(fcfsAnswerService.judgeAnswer(eventSequence, answer)).thenReturn(true); - when(fcfsService.participate(eventSequence, userId)).thenReturn(isWinner); + when(fcfsAnswerService.judgeAnswer(eventId, answer)).thenReturn(true); + when(fcfsService.participate(eventId, userId)).thenReturn(isWinner); String requestBody = mapper.writeValueAsString(answer); String responseBody = mapper.writeValueAsString(responseFcfsResultDto); // when & then - mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/event/fcfs/" + eventSequence) + mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/event/fcfs/" + eventId) .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) .andExpect(status().isOk()) .andExpect(content().json(responseBody)); - verify(fcfsService, times(1)).participate(eventSequence, userId); + verify(fcfsService, times(1)).participate(eventId, userId); } @DisplayName("participate: 정답을 맞히지 못하면 무조건 참여 실패하며 fcfsService에 접근조차 하지 않는다.") @@ -97,45 +97,49 @@ void participateTest(boolean isWinner) throws Exception { void participateWrongAnswerTest() throws Exception { // given ResponseFcfsResultDto responseFcfsResultDto = new ResponseFcfsResultDto(false, false); - when(fcfsAnswerService.judgeAnswer(eventSequence, answer)).thenReturn(false); + when(fcfsAnswerService.judgeAnswer(eventId, answer)).thenReturn(false); String requestBody = mapper.writeValueAsString(answer); String responseBody = mapper.writeValueAsString(responseFcfsResultDto); // when & then - mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/event/fcfs/" + eventSequence) + mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/event/fcfs/" + eventId) .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) .andExpect(status().isOk()) .andExpect(content().json(responseBody)); - verify(fcfsService, never()).participate(eventSequence, userId); + verify(fcfsService, never()).participate(eventId, userId); } @DisplayName("participate: 선착순 이벤트 참여 시 이벤트 시간이 아니어서 예외가 발생하는 경우") @Test void participate400Test() throws Exception { // given - when(fcfsAnswerService.judgeAnswer(eventSequence, answer)).thenReturn(true); - when(fcfsService.participate(eventSequence, userId)).thenThrow(new FcfsEventException(ErrorCode.INVALID_EVENT_TIME)); + when(fcfsAnswerService.judgeAnswer(eventId, answer)).thenReturn(true); + when(fcfsService.participate(eventId, userId)).thenThrow(new FcfsEventException(ErrorCode.INVALID_EVENT_TIME)); String requestBody = mapper.writeValueAsString(answer); String responseBody = mapper.writeValueAsString(ErrorResponse.from(ErrorCode.INVALID_EVENT_TIME)); // when & then - mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/event/fcfs/" + eventSequence) + mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/event/fcfs/" + eventId) .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) .andExpect(status().isBadRequest()) .andExpect(content().json(responseBody)); } - @DisplayName("participate: 선착순 이벤트 참여 시 요청 형식이 잘못된 경우") - @ParameterizedTest(name = "eventSequence: {0}") + @DisplayName("participate: 선착순 이벤트 참여 시 eventId가 잘못된 경우") + @ParameterizedTest(name = "eventId: {0}") @ValueSource(strings = {"a", "1.1", "1.0", "1.1.1"}) - void participateBadInputTest(String eventSequence) throws Exception { + void participateBadInputTest(String eventId) throws Exception { + // given + when(fcfsAnswerService.judgeAnswer(eventId, answer)).thenReturn(true); + when(fcfsService.participate(eventId, userId)).thenThrow(new FcfsEventException(ErrorCode.FCFS_EVENT_NOT_FOUND)); + // when & then - mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/event/fcfs/" + eventSequence) + mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/event/fcfs/" + eventId) .contentType(MediaType.APPLICATION_JSON) .content(mapper.writeValueAsString(answer))) - .andExpect(status().isBadRequest()); + .andExpect(status().isNotFound()); } @DisplayName("getFcfsInfo: 선착순 이벤트에 대한 정보(서버 기준 시각, 이벤트의 상태)를 조회한다.") @@ -143,11 +147,11 @@ void participateBadInputTest(String eventSequence) throws Exception { void getFcfsInfoTest() throws Exception { // given ResponseFcfsInfoDto responseFcfsInfoDto = new ResponseFcfsInfoDto(LocalDateTime.now(), "waiting"); - when(fcfsManageService.getFcfsInfo(eventSequence)).thenReturn(responseFcfsInfoDto); + when(fcfsManageService.getFcfsInfo(eventId)).thenReturn(responseFcfsInfoDto); String responseBody = mapper.writeValueAsString(responseFcfsInfoDto); // when & then - mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/event/fcfs/{eventSequence}/info", eventSequence)) + mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/event/fcfs/{eventId}/info", eventId)) .andExpect(status().isOk()) .andExpect(content().json(responseBody)); } @@ -156,11 +160,11 @@ void getFcfsInfoTest() throws Exception { @Test void getFcfsInfo404Test() throws Exception { // given - when(fcfsManageService.getFcfsInfo(eventSequence)).thenThrow(new FcfsEventException(ErrorCode.EVENT_NOT_FOUND)); + when(fcfsManageService.getFcfsInfo(eventId)).thenThrow(new FcfsEventException(ErrorCode.EVENT_NOT_FOUND)); String responseBody = mapper.writeValueAsString(ErrorResponse.from(ErrorCode.EVENT_NOT_FOUND)); // when & then - mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/event/fcfs/{eventSequence}/info", eventSequence)) + mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/event/fcfs/{eventId}/info", eventId)) .andExpect(status().isNotFound()) .andExpect(content().json(responseBody)); } @@ -169,10 +173,10 @@ void getFcfsInfo404Test() throws Exception { @Test void isParticipatedTest() throws Exception { // given - when(fcfsManageService.isParticipated(eventSequence, userId)).thenReturn(true); + when(fcfsManageService.isParticipated(eventId, userId)).thenReturn(true); // when & then - mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/event/fcfs/" + eventSequence + "/participated")) + mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/event/fcfs/" + eventId + "/participated")) .andExpect(status().isOk()) .andExpect(content().string("true")); } @@ -181,11 +185,11 @@ void isParticipatedTest() throws Exception { @Test void isParticipated404Test() throws Exception { // given - when(fcfsManageService.isParticipated(eventSequence, userId)).thenThrow(new FcfsEventException(ErrorCode.EVENT_NOT_FOUND)); + when(fcfsManageService.isParticipated(eventId, userId)).thenThrow(new FcfsEventException(ErrorCode.EVENT_NOT_FOUND)); String responseBody = mapper.writeValueAsString(ErrorResponse.from(ErrorCode.EVENT_NOT_FOUND)); // when & then - mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/event/fcfs/" + eventSequence + "/participated")) + mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/event/fcfs/" + eventId + "/participated")) .andExpect(status().isNotFound()) .andExpect(content().json(responseBody)); } diff --git a/src/test/java/hyundai/softeer/orange/event/fcfs/FcfsManageServiceTest.java b/src/test/java/hyundai/softeer/orange/event/fcfs/FcfsManageServiceTest.java index 7dcf4654..c016635c 100644 --- a/src/test/java/hyundai/softeer/orange/event/fcfs/FcfsManageServiceTest.java +++ b/src/test/java/hyundai/softeer/orange/event/fcfs/FcfsManageServiceTest.java @@ -3,7 +3,6 @@ import hyundai.softeer.orange.common.ErrorCode; import hyundai.softeer.orange.common.util.ConstantUtil; import hyundai.softeer.orange.event.common.entity.EventFrame; -import hyundai.softeer.orange.event.common.repository.EventFrameRepository; import hyundai.softeer.orange.event.fcfs.dto.ResponseFcfsInfoDto; import hyundai.softeer.orange.event.fcfs.dto.ResponseFcfsWinnerDto; import hyundai.softeer.orange.event.fcfs.entity.FcfsEvent; @@ -50,9 +49,6 @@ class FcfsManageServiceTest { @Mock private FcfsEventWinningInfoRepository fcfsEventWinningInfoRepository; - @Mock - private EventFrameRepository eventFrameRepository; - @Mock private StringRedisTemplate stringRedisTemplate; @@ -65,13 +61,8 @@ class FcfsManageServiceTest { @Mock private ValueOperations valueOperations; - @Mock - private RedisTemplate numberRedisTemplate; - - @Mock - private RedisTemplate booleanRedisTemplate; - - Long eventSequence = 1L; + String eventId = "HD_240808_001"; + Long fcfsEventId = 1L; EventFrame eventFrame = EventFrame.of("the-new-ioniq5","FcfsManageServiceTest"); EventUser eventUser = EventUser.of("test", "0101234567", eventFrame, "uuid"); FcfsEvent fcfsEvent = FcfsEvent.builder() @@ -82,6 +73,8 @@ class FcfsManageServiceTest { @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); + given(stringRedisTemplate.opsForValue()).willReturn(valueOperations); + given(valueOperations.get(eventId)).willReturn(fcfsEventId.toString()); } @DisplayName("registerFcfsEvents: 오늘의 선착순 이벤트 정보(당첨자 수, 시작 시각)를 배치") @@ -105,9 +98,9 @@ void registerWinnersTest() { // given given(stringRedisTemplate.keys("*:fcfs")).willReturn(Set.of("1:fcfs")); given(stringRedisTemplate.opsForZSet()).willReturn(zSetOperations); - given(zSetOperations.range(FcfsUtil.winnerFormatting(eventSequence.toString()), 0, -1)) + given(zSetOperations.range(FcfsUtil.winnerFormatting(fcfsEventId.toString()), 0, -1)) .willReturn(Set.of(eventUser.getUserId())); - given(fcfsEventRepository.findById(eventSequence)).willReturn(Optional.of(fcfsEvent)); + given(fcfsEventRepository.findById(fcfsEventId)).willReturn(Optional.of(fcfsEvent)); given(eventUserRepository.findAllByUserId(List.of(eventUser.getUserId()))).willReturn(List.of(eventUser)); // when @@ -115,8 +108,8 @@ void registerWinnersTest() { // then verify(stringRedisTemplate).keys("*:fcfs"); - verify(zSetOperations).range(FcfsUtil.winnerFormatting(eventSequence.toString()), 0, -1); - verify(fcfsEventRepository).findById(eventSequence); + verify(zSetOperations).range(FcfsUtil.winnerFormatting(fcfsEventId.toString()), 0, -1); + verify(fcfsEventRepository).findById(fcfsEventId); verify(eventUserRepository).findAllByUserId(List.of(eventUser.getUserId())); verify(fcfsEventWinningInfoRepository).saveAll(any()); } @@ -125,12 +118,14 @@ void registerWinnersTest() { @ParameterizedTest @ValueSource(ints = {0, 1, 2, 200, 418, 419}) void getFcfsInfoProgressTest(int minute) { - // when + // given given(stringRedisTemplate.opsForValue()).willReturn(valueOperations); - given(valueOperations.get(FcfsUtil.startTimeFormatting(eventSequence.toString()))) + given(valueOperations.get(eventId)).willReturn(fcfsEventId.toString()); + given(valueOperations.get(FcfsUtil.startTimeFormatting(fcfsEventId.toString()))) .willReturn(LocalDateTime.now().minusMinutes(minute).toString()); - ResponseFcfsInfoDto fcfsInfo = fcfsManageService.getFcfsInfo(eventSequence); + // when + ResponseFcfsInfoDto fcfsInfo = fcfsManageService.getFcfsInfo(eventId); // then assertThat(fcfsInfo.getEventStatus()).isEqualTo(ConstantUtil.PROGRESS); @@ -140,12 +135,13 @@ void getFcfsInfoProgressTest(int minute) { @ParameterizedTest @ValueSource(ints = {1, 2, 3, 120, 178, 179}) void getFcfsInfoCountdownTest(int minute) { - // when + // given given(stringRedisTemplate.opsForValue()).willReturn(valueOperations); - given(valueOperations.get(FcfsUtil.startTimeFormatting(eventSequence.toString()))) + given(valueOperations.get(FcfsUtil.startTimeFormatting(fcfsEventId.toString()))) .willReturn(LocalDateTime.now().plusMinutes(minute).toString()); - ResponseFcfsInfoDto fcfsInfo = fcfsManageService.getFcfsInfo(eventSequence); + // when + ResponseFcfsInfoDto fcfsInfo = fcfsManageService.getFcfsInfo(eventId); // then assertThat(fcfsInfo.getEventStatus()).isEqualTo(ConstantUtil.COUNTDOWN); @@ -155,12 +151,13 @@ void getFcfsInfoCountdownTest(int minute) { @ParameterizedTest @ValueSource(ints = {420, 421, 422}) void getFcfsInfoWaitingTest(int minute) { - // when + // given given(stringRedisTemplate.opsForValue()).willReturn(valueOperations); - given(valueOperations.get(FcfsUtil.startTimeFormatting(eventSequence.toString()))) + given(valueOperations.get(FcfsUtil.startTimeFormatting(fcfsEventId.toString()))) .willReturn(LocalDateTime.now().minusMinutes(minute).toString()); - ResponseFcfsInfoDto fcfsInfo = fcfsManageService.getFcfsInfo(eventSequence); + // when + ResponseFcfsInfoDto fcfsInfo = fcfsManageService.getFcfsInfo(eventId); // then assertThat(fcfsInfo.getEventStatus()).isEqualTo(ConstantUtil.WAITING); @@ -171,10 +168,10 @@ void getFcfsInfoWaitingTest(int minute) { void getFcfsInfoNotFoundTest() { // when given(stringRedisTemplate.opsForValue()).willReturn(valueOperations); - given(valueOperations.get(FcfsUtil.startTimeFormatting(eventSequence.toString()))) + given(valueOperations.get(FcfsUtil.startTimeFormatting(fcfsEventId.toString()))) .willReturn(null); - assertThatThrownBy(() -> fcfsManageService.getFcfsInfo(eventSequence)) + assertThatThrownBy(() -> fcfsManageService.getFcfsInfo(eventId)) .isInstanceOf(FcfsEventException.class) .hasMessage(ErrorCode.FCFS_EVENT_NOT_FOUND.getMessage()); } @@ -183,12 +180,12 @@ void getFcfsInfoNotFoundTest() { @Test void isParticipatedTest() { // given - given(fcfsEventRepository.existsById(eventSequence)).willReturn(true); + given(fcfsEventRepository.existsById(fcfsEventId)).willReturn(true); given(stringRedisTemplate.opsForSet()).willReturn(setOperations); - given(setOperations.isMember(FcfsUtil.participantFormatting(eventSequence.toString()), eventUser.getUserId())).willReturn(true); + given(setOperations.isMember(FcfsUtil.participantFormatting(fcfsEventId.toString()), eventUser.getUserId())).willReturn(true); // when - boolean participated = fcfsManageService.isParticipated(eventSequence, eventUser.getUserId()); + boolean participated = fcfsManageService.isParticipated(eventId, eventUser.getUserId()); // then assertThat(participated).isTrue(); @@ -198,10 +195,10 @@ void isParticipatedTest() { @Test void isParticipatedNotFoundTest() { // given - given(fcfsEventRepository.existsById(eventSequence)).willReturn(false); + given(fcfsEventRepository.existsById(fcfsEventId)).willReturn(false); // when & then - assertThatThrownBy(() -> fcfsManageService.isParticipated(eventSequence, eventUser.getUserId())) + assertThatThrownBy(() -> fcfsManageService.isParticipated(eventId, eventUser.getUserId())) .isInstanceOf(FcfsEventException.class) .hasMessage(ErrorCode.FCFS_EVENT_NOT_FOUND.getMessage()); } @@ -211,11 +208,11 @@ void isParticipatedNotFoundTest() { void getFcfsWinnersInfoTest() { // given LocalDateTime now = LocalDateTime.now(); - given(fcfsEventWinningInfoRepository.findByFcfsEventId(eventSequence)) + given(fcfsEventWinningInfoRepository.findByFcfsEventId(fcfsEventId)) .willReturn(List.of(FcfsEventWinningInfo.of(fcfsEvent, eventUser, now))); // when - List fcfsWinnersInfo = fcfsManageService.getFcfsWinnersInfo(eventSequence); + List fcfsWinnersInfo = fcfsManageService.getFcfsWinnersInfo(fcfsEventId); // then assertThat(fcfsWinnersInfo).hasSize(1); diff --git a/src/test/java/hyundai/softeer/orange/load/DbFcfsServiceLoadTest.java b/src/test/java/hyundai/softeer/orange/load/DbFcfsServiceLoadTest.java index d82047be..607113a6 100644 --- a/src/test/java/hyundai/softeer/orange/load/DbFcfsServiceLoadTest.java +++ b/src/test/java/hyundai/softeer/orange/load/DbFcfsServiceLoadTest.java @@ -1,5 +1,7 @@ package hyundai.softeer.orange.load; +import hyundai.softeer.orange.event.common.entity.EventMetadata; +import hyundai.softeer.orange.event.common.repository.EventMetadataRepository; import hyundai.softeer.orange.event.fcfs.entity.FcfsEvent; import hyundai.softeer.orange.event.fcfs.repository.FcfsEventRepository; import hyundai.softeer.orange.event.fcfs.repository.FcfsEventWinningInfoRepository; @@ -11,8 +13,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.test.context.TestPropertySource; import java.time.LocalDateTime; @@ -45,6 +49,13 @@ class DbFcfsServiceLoadTest { Long numberOfWinners = 100L; int numberOfThreads = 200; // 스레드 수 int numberOfUsers = 1000; // 동시 참여 사용자 수 + String eventId = "HD_240808_001"; + + @Autowired + private EventMetadataRepository eventMetadataRepository; + @Qualifier("stringRedisTemplate") + @Autowired + private StringRedisTemplate stringRedisTemplate; @BeforeEach void setUp() { @@ -53,10 +64,16 @@ void setUp() { eventUserRepository.deleteAll(); fcfsEventRepository.deleteAll(); booleanRedisTemplate.getConnectionFactory().getConnection().flushAll(); + stringRedisTemplate.getConnectionFactory().getConnection().flushAll(); // 이벤트 생성 - FcfsEvent fcfsEvent = FcfsEvent.of(LocalDateTime.now(), LocalDateTime.now().plusDays(1), numberOfWinners, "prizeInfo", null); + EventMetadata eventMetadata = EventMetadata.builder() + .eventId(eventId) + .build(); + eventMetadataRepository.save(eventMetadata); + FcfsEvent fcfsEvent = FcfsEvent.of(LocalDateTime.now(), LocalDateTime.now().plusDays(1), numberOfWinners, "prizeInfo", eventMetadata); fcfsEventRepository.save(fcfsEvent); + stringRedisTemplate.opsForValue().set(eventId, fcfsEvent.getId().toString()); // 유저 생성 for (int i = 0; i < numberOfUsers; i++) { @@ -81,7 +98,7 @@ void participateTest() throws InterruptedException { final int index = i; executorService.execute(() -> { try { - boolean result = dbFcfsService.participate(1L, "user" + index); + boolean result = dbFcfsService.participate(eventId, "user" + index); } catch (Exception e) { e.printStackTrace(); } finally { diff --git a/src/test/java/hyundai/softeer/orange/load/RedisLockFcfsServiceLoadTest.java b/src/test/java/hyundai/softeer/orange/load/RedisLockFcfsServiceLoadTest.java index 3d00a9ed..99dd9ba6 100644 --- a/src/test/java/hyundai/softeer/orange/load/RedisLockFcfsServiceLoadTest.java +++ b/src/test/java/hyundai/softeer/orange/load/RedisLockFcfsServiceLoadTest.java @@ -35,6 +35,7 @@ class RedisLockFcfsServiceLoadTest { Long eventSequence = 1L; // 테스트할 이벤트 시퀀스 int numberOfWinners = 100; // 당첨자 수 + String eventId = "HD_240808_001"; // 이벤트 ID @BeforeEach void setUp() { @@ -46,6 +47,7 @@ void setUp() { booleanRedisTemplate.opsForValue().set(FcfsUtil.endFlagFormatting(eventSequence.toString()), false); numberRedisTemplate.opsForValue().set(FcfsUtil.keyFormatting(eventSequence.toString()), numberOfWinners); stringRedisTemplate.opsForValue().set(FcfsUtil.startTimeFormatting(eventSequence.toString()), "2021-08-01T00:00:00"); + stringRedisTemplate.opsForValue().set(eventId, eventSequence.toString()); } @AfterEach @@ -72,7 +74,7 @@ void loadTestParticipate() throws InterruptedException { final int index = i; executorService.execute(() -> { try { - boolean result = redisLockFcfsService.participate(1L, "user" + index); + boolean result = redisLockFcfsService.participate(eventId, "user" + index); } catch (Exception e) { e.printStackTrace(); } finally { diff --git a/src/test/java/hyundai/softeer/orange/load/RedisLuaFcfsServiceLoadTest.java b/src/test/java/hyundai/softeer/orange/load/RedisLuaFcfsServiceLoadTest.java index 5a2777e4..7ea06e4b 100644 --- a/src/test/java/hyundai/softeer/orange/load/RedisLuaFcfsServiceLoadTest.java +++ b/src/test/java/hyundai/softeer/orange/load/RedisLuaFcfsServiceLoadTest.java @@ -32,6 +32,7 @@ class RedisLuaFcfsServiceLoadTest { Long eventSequence = 1L; // 테스트할 이벤트 시퀀스 int numberOfWinners = 100; // 당첨자 수 + String eventId = "HD_240808_001"; // 이벤트 ID @BeforeEach void setUp() { @@ -43,6 +44,7 @@ void setUp() { // 테스트할 이벤트 정보 저장 numberRedisTemplate.opsForValue().set(FcfsUtil.keyFormatting(eventSequence.toString()), numberOfWinners); stringRedisTemplate.opsForValue().set(FcfsUtil.startTimeFormatting(eventSequence.toString()), "2021-08-01T00:00:00"); + stringRedisTemplate.opsForValue().set(eventId, eventSequence.toString()); } @Test @@ -58,7 +60,7 @@ void participateTest() throws InterruptedException { final int index = i; executorService.execute(() -> { try { - boolean result = redisLuaFcfsService.participate(1L, "user" + index); + boolean result = redisLuaFcfsService.participate(eventId, "user" + index); } catch (Exception e) { e.printStackTrace(); } finally { diff --git a/src/test/java/hyundai/softeer/orange/load/RedisSetFcfsServiceLoadTest.java b/src/test/java/hyundai/softeer/orange/load/RedisSetFcfsServiceLoadTest.java index e6d521ed..509d40d1 100644 --- a/src/test/java/hyundai/softeer/orange/load/RedisSetFcfsServiceLoadTest.java +++ b/src/test/java/hyundai/softeer/orange/load/RedisSetFcfsServiceLoadTest.java @@ -35,6 +35,7 @@ class RedisSetFcfsServiceLoadTest { Long eventSequence = 1L; // 테스트할 이벤트 시퀀스 int numberOfWinners = 100; // 당첨자 수 + String eventId = "HD_240808_001"; // 이벤트 ID @BeforeEach void setUp() { @@ -47,6 +48,7 @@ void setUp() { booleanRedisTemplate.opsForValue().set(FcfsUtil.endFlagFormatting(eventSequence.toString()), false); numberRedisTemplate.opsForValue().set(FcfsUtil.keyFormatting(eventSequence.toString()), numberOfWinners); stringRedisTemplate.opsForValue().set(FcfsUtil.startTimeFormatting(eventSequence.toString()), "2021-08-01T00:00:00"); + stringRedisTemplate.opsForValue().set(eventId, eventSequence.toString()); } @AfterEach @@ -70,7 +72,7 @@ void participateTest() throws InterruptedException { final int index = i; executorService.execute(() -> { try { - boolean result = redisSetFcfsService.participate(1L, "user" + index); + boolean result = redisSetFcfsService.participate(eventId, "user" + index); } catch (Exception e) { e.printStackTrace(); } finally {