Skip to content

Commit

Permalink
Merge pull request #78 from Team-BomBomBom/refactor/algorithm_assignm…
Browse files Browse the repository at this point in the history
…ent_handling_logic_branching#BBB-162

Refactor: #BBB-162 호출 초과 시점에만 대기열 큐에 알고리즘 과제 관련 요청 파라미터 추가
  • Loading branch information
platinouss authored Dec 11, 2024
2 parents 3faaa56 + 2be4ebb commit 8cd1436
Show file tree
Hide file tree
Showing 18 changed files with 377 additions and 307 deletions.
Original file line number Diff line number Diff line change
@@ -1,68 +1,42 @@
package com.bombombom.devs.external.algo.service;

import com.bombombom.devs.algo.model.AlgorithmProblem;
import com.bombombom.devs.algo.model.vo.AlgorithmProblemQueueMessage;
import com.bombombom.devs.algo.model.vo.AlgorithmAssignmentQueueMessage;
import com.bombombom.devs.algo.model.vo.AlgorithmTaskUpdateStatus;
import com.bombombom.devs.algo.model.vo.AssignAlgorithmProblem;
import com.bombombom.devs.algo.model.vo.AssignAlgorithmProblemMessage;
import com.bombombom.devs.algo.model.vo.PendingMessageInfo;
import com.bombombom.devs.algo.model.vo.UpdateAlgorithmTaskStatusMessage;
import com.bombombom.devs.algo.repository.AlgorithmProblemRedisQueueRepository;
import com.bombombom.devs.algo.repository.AlgorithmProblemRepository;
import com.bombombom.devs.core.enums.AlgoTag;
import com.bombombom.devs.core.exception.BusinessRuleException;
import com.bombombom.devs.core.exception.ErrorCode;
import com.bombombom.devs.core.exception.NotFoundException;
import com.bombombom.devs.core.exception.ServerInternalException;
import com.bombombom.devs.core.util.Clock;
import com.bombombom.devs.external.algo.service.dto.command.AssignAlgorithmProblemCommand;
import com.bombombom.devs.external.algo.service.dto.command.UpdateAlgorithmTaskStatusCommand;
import com.bombombom.devs.job.AlgorithmProblemConverter;
import com.bombombom.devs.solvedac.SolvedacClient;
import com.bombombom.devs.solvedac.dto.ProblemListResponse;
import com.bombombom.devs.solvedac.dto.ProblemResponse;
import com.bombombom.devs.study.model.AlgorithmProblemAssignment;
import com.bombombom.devs.study.model.AlgorithmProblemSolvedHistory;
import com.bombombom.devs.study.model.AlgorithmStudy;
import com.bombombom.devs.study.model.Round;
import com.bombombom.devs.study.model.Study;
import com.bombombom.devs.study.repository.AlgorithmProblemAssignmentRepository;
import com.bombombom.devs.study.repository.AlgorithmProblemSolvedHistoryRepository;
import com.bombombom.devs.study.repository.RoundRepository;
import com.bombombom.devs.user.model.User;
import com.bombombom.devs.user.repository.UserRepository;
import com.bombombom.devs.external.study.service.AlgorithmStudyService;
import com.bombombom.devs.external.study.service.dto.command.CheckAlgorithmProblemSolvedCommand;
import com.fasterxml.jackson.core.JsonProcessingException;
import jakarta.annotation.PostConstruct;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
* '알고리즘 과제 할당 요청'과 '알고리즘 과제 해결 여부 갱신 요청' 시, 각각
* {@link AlgorithmProblemQueueService#addAssignProblemRequest(Study, AlgorithmStudy, Round, Map)}
* 메서드와 {@link AlgorithmProblemQueueService#addUpdateTaskStatusRequest(User, List, Long)} 메서드가
* 호출된다.
* '알고리즘 과제 할당 요청'과 '알고리즘 과제 해결 여부 갱신 요청' 처리 시 외부 API 호출 초과 시점이라면, 추후 해당 요청을 처리하기 위해 대기열 큐에 요청 파라미터를
* 추가하는 {@link AlgorithmProblemQueueService#addAssignProblemRequest(AssignAlgorithmProblemMessage)}
* 메서드와
* {@link AlgorithmProblemQueueService#addUpdateTaskStatusRequest(UpdateAlgorithmTaskStatusMessage)}
* 메서드가 호출된다.
* <p>
* 이 두 가지 요청은, 외부 API(solved.ac API)의 호출 제한 조건(15분에 256회)으로 인해 Token Bucket 알고리즘 기반 Rate Limit이
* 적용되며, 대기열 큐에 해당 요청을 추가한다.
* ({@link com.bombombom.devs.ratelimit.config.SolvedacApiRateLimitConfig
* SolvedacApiRateLimitConfig} 에서 설정된 수치를 바탕으로 Rate Limit이 적용)
* <p>
* {@link com.bombombom.devs.job.AlgorithmStudyAssignmentJob AlgorithmStudyAssignmentJob}을 수행하는
* 스케줄러는 대기열 큐에 담긴 메시지를 순차적으로 읽고, 메시지 타입(과제 할당 또는 갱신 요청)에 따라 각각
* {@link AlgorithmProblemQueueService#assignProblems(AssignAlgorithmProblemCommand)}와
* {@link AlgorithmProblemQueueService#updateTaskStatus(UpdateAlgorithmTaskStatusCommand)}를 호출하여, 과제
* 할당 또는 해결 여부 갱신 요청 로직을 수행한다. 이 두 메서드는 Rate Limit을 관리하는 Bucket에 Token이 존재하는 경우에만 수행된다.
* ({@link com.bombombom.devs.ratelimit.config.SolvedacApiRateLimitConfig
* SolvedacApiRateLimitConfig} 내부에 Aspect가 선언되어 Token consume 로직 수행)</p>
* 이후 {@link com.bombombom.devs.job.AlgorithmStudyAssignmentJob AlgorithmStudyAssignmentJob}을 수행하는
* 스케줄러는 대기열 큐에 담긴 메시지를 순차적으로 읽고, 메시지 타입(과제 할당 또는 해결 여부 갱신 요청)에 따라 각각 실제 로직을 수행하는
* {@link AlgorithmStudyService#assignAlgorithmProblems(AssignAlgorithmProblemCommand)}와
* {@link AlgorithmStudyService#updateAlgorithmTaskStatus(CheckAlgorithmProblemSolvedCommand)}를
* 호출하여, '알고리즘 과제 할당' 또는 '알고리즘 과제 해결 여부 갱신' 요청을 처리한다.</p>
*
* @see <a href="https://github.com/Team-BomBomBom/Server/pull/51">Feat: #BBB-120 알고리즘 과제 할당 및 해결 여부
* 요청에 Rate Limit과 대기열 시스템 적용</a>
* @see <a href="https://github.com/Team-BomBomBom/Server/pull/78">Refactor: #BBB-162 호출 초과 시점에만 대기열
* 큐에 알고리즘 과제 관련 요청 파라미터 추가</a>
*/

@Slf4j
Expand All @@ -74,112 +48,47 @@ public class AlgorithmProblemQueueService {
static final long PENDING_MESSAGE_PROCESSING_INTERVAL_MS = 60 * 1000;

private final Clock clock;
private final SolvedacClient solvedacClient;
private final AlgorithmProblemConverter algorithmProblemConverter;
private final AlgorithmProblemService algorithmProblemService;
private final UserRepository userRepository;
private final RoundRepository roundRepository;
private final AlgorithmProblemRepository algorithmProblemRepository;
private final AlgorithmProblemRedisQueueRepository algorithmProblemRedisQueueRepository;
private final AlgorithmProblemAssignmentRepository algorithmProblemAssignmentRepository;
private final AlgorithmProblemSolvedHistoryRepository algorithmProblemSolvedHistoryRepository;

@PostConstruct
void init() {
algorithmProblemRedisQueueRepository.createConsumerGroup();
}

@Transactional
public void addUpdateTaskStatusRequest(User user, List<AlgorithmProblem> problems,
Long studyId) {
if (hasRecentlyUpdatedTaskStatus(studyId, user.getId())) {
throw new BusinessRuleException(ErrorCode.ALGORITHM_TASK_STATUS_RECENTLY_UPDATED);
}
public void addUpdateTaskStatusRequest(UpdateAlgorithmTaskStatusMessage message) {
try {
Set<Integer> problemRefIds = problems.stream().map(AlgorithmProblem::getRefId)
.collect(Collectors.toSet());
algorithmProblemRedisQueueRepository.addMessage(studyId, user.getId(),
user.getBaekjoon(), problemRefIds);
algorithmProblemRedisQueueRepository.setTaskUpdateInProgress(studyId,
user.getId());
algorithmProblemRedisQueueRepository.addMessage(message);
} catch (JsonProcessingException e) {
throw new ServerInternalException(ErrorCode.JSON_CONVERSION_FAIL);
}
}

@Transactional
public void addAssignProblemRequest(Study study, AlgorithmStudy algorithmStudy, Round round,
Map<AlgoTag, Integer> problemCountForEachTag) {
public void addAssignProblemRequest(AssignAlgorithmProblemMessage message) {
try {
algorithmProblemRedisQueueRepository.addMessage(
AssignAlgorithmProblem.of(study, algorithmStudy, round, problemCountForEachTag));
algorithmProblemRedisQueueRepository.addMessage(message);
} catch (JsonProcessingException e) {
throw new ServerInternalException(ErrorCode.JSON_CONVERSION_FAIL);
}
}

@Transactional
public void updateTaskStatus(UpdateAlgorithmTaskStatusCommand command) {
try {
ProblemListResponse solvedProblems = solvedacClient.checkProblemSolved(
command.baekjoonId(), command.problemRefIds());
List<Integer> problemRefIds = solvedProblems.items().stream()
.map(ProblemResponse::problemId).toList();
User user = userRepository.findById(command.userId())
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND));
List<AlgorithmProblem> problems = algorithmProblemRepository.findAllByRefId(
problemRefIds);
Set<Long> solvedProblemIds = algorithmProblemSolvedHistoryRepository.findByUserIdAndProblemIds(
user.getId(), problems.stream().map(AlgorithmProblem::getId).toList())
.stream().map(history -> history.getProblem().getId()).collect(Collectors.toSet());
List<AlgorithmProblemSolvedHistory> histories = problems.stream()
.filter(problem -> !solvedProblemIds.contains(problem.getId()))
.map(problem -> AlgorithmProblemSolvedHistory.createAlgorithmProblemSolvedHistory(
user, problem, command.requestTime())).toList();
algorithmProblemSolvedHistoryRepository.saveAll(histories);
algorithmProblemRedisQueueRepository.setTaskUpdateCompleted(command.studyId(),
command.userId());
algorithmProblemRedisQueueRepository.ackMessage(command.recordId());
} catch (NotFoundException e) {
algorithmProblemRedisQueueRepository.setTaskUpdateCompleted(command.studyId(),
command.userId());
algorithmProblemRedisQueueRepository.ackMessage(command.recordId());
}
}

@Transactional
public void assignProblems(AssignAlgorithmProblemCommand command) {
try {
Round round = roundRepository.findById(command.roundId())
.orElseThrow(() -> new NotFoundException(ErrorCode.ROUND_NOT_FOUND));
ProblemListResponse problemListResponse = solvedacClient.getUnSolvedProblems(
command.baekjoonIds(), command.difficultySpread(),
command.problemCountForEachTag());
List<AlgorithmProblem> problems = algorithmProblemConverter.convert(
problemListResponse);
List<AlgorithmProblem> foundOrSavedProblems = algorithmProblemService.findProblemsThenSaveWhenNotExist(
problems);
assignProblemToRound(round, foundOrSavedProblems);
algorithmProblemRedisQueueRepository.ackMessage(command.recordId());
} catch (NotFoundException e) {
algorithmProblemRedisQueueRepository.ackMessage(command.recordId());
}
public void completeAssignProblems(String recordId) {
algorithmProblemRedisQueueRepository.ackMessage(recordId);
}

@Transactional
public void assignProblemToRound(Round round, List<AlgorithmProblem> problems) {
List<AlgorithmProblemAssignment> assignments = new ArrayList<>();
for (AlgorithmProblem problem : problems) {
assignments.add(AlgorithmProblemAssignment.of(round, problem));
}
algorithmProblemAssignmentRepository.saveAll(assignments);
public void completeUpdateTaskStatus(Long studyId, Long userId, String recordId) {
algorithmProblemRedisQueueRepository.setTaskUpdateCompleted(studyId, userId);
algorithmProblemRedisQueueRepository.ackMessage(recordId);
}

public AlgorithmProblemQueueMessage getAssignOrTaskStatusUpdateMessage() {
public AlgorithmAssignmentQueueMessage getAssignOrTaskStatusUpdateMessage() {
return algorithmProblemRedisQueueRepository.readMessage();
}

public AlgorithmProblemQueueMessage getUnprocessedAssignOrTaskStatusUpdateMessage() {
public AlgorithmAssignmentQueueMessage getUnprocessedAssignOrTaskStatusUpdateMessage() {
PendingMessageInfo pendingMessageInfo = algorithmProblemRedisQueueRepository.getOldestPendingMessageInfo();
if (pendingMessageInfo == null || pendingMessageInfo.elapsedTime().toMillis()
< PENDING_MESSAGE_PROCESSING_INTERVAL_MS) {
Expand All @@ -189,7 +98,7 @@ public AlgorithmProblemQueueMessage getUnprocessedAssignOrTaskStatusUpdateMessag
pendingMessageInfo);
}

private boolean hasRecentlyUpdatedTaskStatus(Long studyId, Long userId) {
public boolean hasRecentlyUpdatedTaskStatus(Long studyId, Long userId) {
AlgorithmTaskUpdateStatus taskUpdateStatus = algorithmProblemRedisQueueRepository.getTaskUpdateStatus(
studyId, userId);
if (taskUpdateStatus == null || taskUpdateStatus.statusUpdatedAt() == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.bombombom.devs.external.algo.service.dto.command;

import com.bombombom.devs.algo.model.vo.UpdateAlgorithmTaskStatusMessage;
import com.bombombom.devs.external.study.service.dto.command.CheckAlgorithmProblemSolvedCommand;
import java.util.Set;
import lombok.Builder;

@Builder
public record AddSolvedProblemHistoriesCommand(
Long userId,
Long studyId,
Set<Long> problemIds
) {

public static AddSolvedProblemHistoriesCommand of(CheckAlgorithmProblemSolvedCommand command) {
return AddSolvedProblemHistoriesCommand.builder()
.userId(command.userId())
.problemIds(command.problemIds())
.build();
}

public static AddSolvedProblemHistoriesCommand fromMessage(
UpdateAlgorithmTaskStatusMessage message) {
return AddSolvedProblemHistoriesCommand.builder()
.userId(message.userId())
.studyId(message.studyId())
.problemIds(message.problemIds())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,37 +1,50 @@
package com.bombombom.devs.external.algo.service.dto.command;

import com.bombombom.devs.algo.model.vo.AlgorithmProblemQueueMessage;
import com.bombombom.devs.algo.model.vo.AssignAlgorithmProblem;
import com.bombombom.devs.algo.model.vo.AssignAlgorithmProblemMessage;
import com.bombombom.devs.core.Spread;
import com.bombombom.devs.core.enums.AlgoTag;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.LocalDateTime;
import com.bombombom.devs.study.model.AlgorithmStudy;
import com.bombombom.devs.study.model.Round;
import com.bombombom.devs.study.model.Study;
import java.util.Map;
import java.util.Set;
import lombok.Builder;

@Builder
public record AssignAlgorithmProblemCommand(
String recordId,
LocalDateTime requestTime,
Long roundId,
Round round,
Set<String> baekjoonIds,
Map<AlgoTag, Spread> difficultySpread,
Map<AlgoTag, Integer> problemCountForEachTag
) {

public static AssignAlgorithmProblemCommand fromMessage(AlgorithmProblemQueueMessage message,
ObjectMapper objectMapper) throws JsonProcessingException {
AssignAlgorithmProblem assignAlgorithmProblem = objectMapper.readValue(
message.fields(), AssignAlgorithmProblem.class);
public static AssignAlgorithmProblemCommand of(Study study, Round round,
Map<AlgoTag, Integer> problemCountForEachTag) {
AlgorithmStudy algorithmStudy = (AlgorithmStudy) study;
return AssignAlgorithmProblemCommand.builder()
.recordId(message.recordId())
.requestTime(message.requestTime())
.roundId(assignAlgorithmProblem.roundId())
.baekjoonIds(assignAlgorithmProblem.baekjoonIds())
.difficultySpread(assignAlgorithmProblem.difficultySpread())
.problemCountForEachTag(assignAlgorithmProblem.problemCountForEachTag())
.round(round)
.baekjoonIds(study.getBaekjoonIds())
.difficultySpread(algorithmStudy.getDifficultySpreadMap())
.problemCountForEachTag(problemCountForEachTag)
.build();
}

public static AssignAlgorithmProblemCommand fromMessage(AssignAlgorithmProblemMessage message,
Round round) {
return AssignAlgorithmProblemCommand.builder()
.round(round)
.baekjoonIds(message.baekjoonIds())
.difficultySpread(message.difficultySpread())
.problemCountForEachTag(message.problemCountForEachTag())
.build();
}

public AssignAlgorithmProblemMessage toVo() {
return AssignAlgorithmProblemMessage.builder()
.roundId(round.getId())
.baekjoonIds(baekjoonIds)
.difficultySpread(difficultySpread)
.problemCountForEachTag(problemCountForEachTag)
.build();
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
import com.bombombom.devs.external.study.service.dto.command.CheckAlgorithmProblemSolvedCommand;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.util.List;
import java.util.Set;

public record CheckAlgorithmProblemSolvedRequest(
@NotNull Long studyId,
@NotNull Integer roundIdx,
@Size(min = 1) List<Long> problemIds,
@Size(min = 1) Set<Long> problemIds,
@NotNull Long userId
) {

Expand Down
Loading

0 comments on commit 8cd1436

Please sign in to comment.