Skip to content

Commit 9c93a71

Browse files
thezz9minjee2758chat26666pokerbearkr
authored
refactor : submission-stream (#61)
* build : update build.gradle for Redis * refactor : Judge0 비동기 병렬 요청 구현 * refactor : 비동기 병렬 요청 구현을 위한 Application, dto * refactor : 비동기 병렬 요청 구현을 위한 config 파일 * feat : Redis Stream 구현 * refactor : 병렬 요청에서 SSE 구별을 위한 인메모리 저장 * refactor : 테스트용 UI * refactor : 그룹핑 처리 DTO로 책임 분리 * refactor : Judge0 polling 예외처리 고도화 * refactor : api 경로 수정 * refactor : 서비스 로직 리팩토링 * refactor : 모델 객체 추가 * refactor : 예외 코드 추가 * Refactor : html(google oauth)로 리다이렉팅되는 문제 해결 (#57) * refactor : logout HttpServeletRequest를 컨트롤러에서 String으로 추출 후 service로 전달 * refactor : whitelist 등록 * refactor : 에러 발생 시, html응답 대신 Json응답으로 받을 수 있도록 리팩토링 * refactor : requestMapping으로 앞에 일괄적으로 /api 달기 * feat : 게임 캐릭터 생성 기능, 아이템 생성 기능, 아이템 뽑기, 스테이터스 확인, 아이템 장착 기능 추가 (#58) * feat : 게임 도메인 엔티티 생성 및 스킬, 아이템 효과 정의 * feat : 게임 도메인 서비스, 아이템 뽑기 도메인서비스 추가, 랜덤 인카운터 추가 * feat : 아이템 장착, 뽑기, 인벤토리 오픈, 스탯 확인, 캐릭터 생성 기능 추가 # Conflicts: # src/main/java/org/ezcode/codetest/common/security/config/SecurityConfig.java * chore : properties 수정 * chore : properties 수정 * chore : securityconfig 오타수정 * chore : 오타수정 * chore : 오타수정 * chore : 무수히 많은 오타수정, 누락된 어노테이션 추가 * docs : 환경변수 추가 * docs : 오타수정 --------- Co-authored-by: pokerbearkr <[email protected]> * Refactor : Filter WhiteList 수정 (#59) * refactor : api 경로 추가, oauth 경로 수정 * refactor : whitelist 경로 수정 * refactor : redirect url 수정 * refactor : Submission Stream --------- Co-authored-by: MIN <[email protected]> Co-authored-by: chat26666 <[email protected]> Co-authored-by: pokerbearkr <[email protected]>
1 parent a119515 commit 9c93a71

File tree

23 files changed

+587
-111
lines changed

23 files changed

+587
-111
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dependencies {
3333

3434
// 레디스
3535
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
36+
implementation 'org.springframework.data:spring-data-redis:3.1.0'
3637

3738
// activemq
3839
implementation 'org.springframework.boot:spring-boot-starter-activemq'

src/main/java/org/ezcode/codetest/application/submission/dto/response/submission/GroupedSubmissionResponse.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package org.ezcode.codetest.application.submission.dto.response.submission;
22

3+
import java.util.Comparator;
34
import java.util.List;
5+
import java.util.stream.Collectors;
46

57
import org.ezcode.codetest.domain.problem.model.entity.Problem;
68
import org.ezcode.codetest.domain.submission.model.entity.Submission;
@@ -24,7 +26,24 @@ public class GroupedSubmissionResponse {
2426
@Schema(description = "해당 문제에 대한 제출 목록")
2527
private final List<SubmissionDetailResponse> submissions;
2628

27-
public GroupedSubmissionResponse(Problem problem, List<Submission> submissions) {
29+
public static List<GroupedSubmissionResponse> groupByProblem(List<Submission> submissions) {
30+
return submissions.stream()
31+
.collect(Collectors.groupingBy(Submission::getProblem))
32+
.entrySet()
33+
.stream()
34+
.map(entry -> createSorted(entry.getKey(), entry.getValue()))
35+
.toList();
36+
}
37+
38+
private static GroupedSubmissionResponse createSorted(Problem problem, List<Submission> submissions) {
39+
List<Submission> sorted = submissions.stream()
40+
.sorted(Comparator.comparing(Submission::getCreatedAt).reversed())
41+
.toList();
42+
43+
return new GroupedSubmissionResponse(problem, sorted);
44+
}
45+
46+
private GroupedSubmissionResponse(Problem problem, List<Submission> submissions) {
2847
this.problemId = problem.getId();
2948
this.problemDescription = problem.getDescription();
3049
this.submissions = submissions.stream()
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package org.ezcode.codetest.application.submission.model;
2+
3+
import org.ezcode.codetest.application.submission.dto.response.submission.FinalResultResponse;
4+
import org.ezcode.codetest.domain.submission.model.SubmissionAggregator;
5+
6+
import java.util.concurrent.CountDownLatch;
7+
import java.util.concurrent.atomic.AtomicInteger;
8+
import java.util.concurrent.atomic.AtomicReference;
9+
10+
public record SubmissionContext(
11+
12+
SubmissionAggregator aggregator,
13+
14+
AtomicInteger passedCount,
15+
16+
AtomicInteger processedCount,
17+
18+
AtomicReference<String> message,
19+
20+
CountDownLatch latch
21+
) {
22+
public static SubmissionContext initialize(int totalTestcaseCount) {
23+
return new SubmissionContext(
24+
new SubmissionAggregator(),
25+
new AtomicInteger(0),
26+
new AtomicInteger(0),
27+
new AtomicReference<>("Accepted"),
28+
new CountDownLatch(totalTestcaseCount)
29+
);
30+
}
31+
32+
public FinalResultResponse toFinalResult(int totalTestcaseCount) {
33+
return new FinalResultResponse(
34+
totalTestcaseCount,
35+
this.getProcessedCount(),
36+
this.getCurrentMessage()
37+
);
38+
}
39+
40+
public void incrementPassedCount() {
41+
this.passedCount.incrementAndGet();
42+
}
43+
44+
public void incrementProcessedCount() {
45+
this.processedCount.incrementAndGet();
46+
}
47+
48+
public int getPassedCount() {
49+
return this.passedCount.get();
50+
}
51+
52+
public int getProcessedCount() {
53+
return this.processedCount.get();
54+
}
55+
56+
public String getCurrentMessage() {
57+
return this.message.get();
58+
}
59+
60+
public void updateMessage(String message) {
61+
this.message.set(message);
62+
}
63+
64+
public void countDown() {
65+
this.latch.countDown();
66+
}
67+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.ezcode.codetest.application.submission.port;
2+
3+
import java.util.Optional;
4+
5+
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
6+
7+
public interface EmitterStore {
8+
9+
void save(String key, SseEmitter emitter);
10+
11+
Optional<SseEmitter> get(String key);
12+
13+
void remove(String key);
14+
15+
}

src/main/java/org/ezcode/codetest/application/submission/port/JudgeClient.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
public interface JudgeClient {
77

8-
JudgeResult execute(CodeCompileRequest request);
8+
String submitAndGetToken(CodeCompileRequest request);
9+
10+
JudgeResult pollUntilDone(String token);
911

1012
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.ezcode.codetest.application.submission.port;
2+
3+
import org.ezcode.codetest.infrastructure.event.dto.SubmissionMessage;
4+
5+
public interface QueueProducer {
6+
7+
void enqueue(SubmissionMessage submissionMessage);
8+
9+
}
Lines changed: 114 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
11
package org.ezcode.codetest.application.submission.service;
22

3-
import java.util.Comparator;
3+
import java.io.IOException;
44
import java.util.List;
5-
import java.util.stream.Collectors;
5+
import java.util.UUID;
6+
import java.util.concurrent.CompletableFuture;
7+
import java.util.concurrent.Executor;
8+
import java.util.concurrent.TimeUnit;
69

710
import org.ezcode.codetest.application.submission.dto.request.review.CodeReviewRequest;
811
import org.ezcode.codetest.application.submission.dto.request.review.ReviewPayload;
912
import org.ezcode.codetest.application.submission.dto.response.review.CodeReviewResponse;
10-
import org.ezcode.codetest.application.submission.dto.response.submission.FinalResultResponse;
1113
import org.ezcode.codetest.application.submission.dto.response.submission.GroupedSubmissionResponse;
14+
import org.ezcode.codetest.application.submission.model.JudgeResult;
1215
import org.ezcode.codetest.application.submission.model.ReviewResult;
16+
import org.ezcode.codetest.application.submission.model.SubmissionContext;
17+
import org.ezcode.codetest.application.submission.port.QueueProducer;
18+
import org.ezcode.codetest.domain.submission.exception.SubmissionException;
19+
import org.ezcode.codetest.domain.submission.exception.code.SubmissionExceptionCode;
20+
import org.ezcode.codetest.domain.submission.model.TestcaseEvaluationInput;
21+
import org.ezcode.codetest.infrastructure.event.dto.SubmissionMessage;
22+
import org.ezcode.codetest.application.submission.port.EmitterStore;
1323
import org.ezcode.codetest.application.submission.port.ReviewClient;
1424
import org.ezcode.codetest.domain.problem.model.ProblemInfo;
15-
import org.ezcode.codetest.domain.submission.model.SubmissionAggregator;
1625
import org.ezcode.codetest.domain.submission.dto.AnswerEvaluation;
1726
import org.ezcode.codetest.domain.submission.dto.SubmissionData;
1827
import org.ezcode.codetest.application.submission.dto.request.compile.CodeCompileRequest;
1928
import org.ezcode.codetest.application.submission.dto.request.submission.CodeSubmitRequest;
20-
import org.ezcode.codetest.application.submission.model.JudgeResult;
2129
import org.ezcode.codetest.application.submission.dto.response.submission.JudgeResultResponse;
2230
import org.ezcode.codetest.application.submission.port.JudgeClient;
2331
import org.ezcode.codetest.domain.language.model.entity.Language;
@@ -30,106 +38,126 @@
3038
import org.ezcode.codetest.domain.user.model.entity.AuthUser;
3139
import org.ezcode.codetest.domain.user.model.entity.User;
3240
import org.ezcode.codetest.domain.user.service.UserDomainService;
41+
import org.springframework.scheduling.annotation.Async;
42+
import org.springframework.security.core.context.SecurityContextHolder;
3343
import org.springframework.stereotype.Service;
3444
import org.springframework.transaction.annotation.Transactional;
3545
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
3646

3747
import lombok.RequiredArgsConstructor;
48+
import lombok.extern.slf4j.Slf4j;
3849

50+
@Slf4j
3951
@Service
4052
@RequiredArgsConstructor
4153
public class SubmissionService {
4254

43-
private final JudgeClient judgeClient;
44-
private final ReviewClient reviewClient;
45-
private final UserDomainService userDomainService;
46-
private final ProblemDomainService problemDomainService;
47-
private final LanguageDomainService languageDomainService;
48-
private final SubmissionDomainService submissionDomainService;
49-
50-
private static final String COMPILE_MESSAGE = "Accepted";
51-
52-
public SseEmitter submitCodeStream(Long problemId, CodeSubmitRequest request, AuthUser authUser) {
53-
54-
SseEmitter emitter = new SseEmitter();
55-
56-
new Thread(() -> {
57-
try {
58-
SubmissionAggregator aggregator = new SubmissionAggregator();
59-
User user = userDomainService.getUserById(authUser.getId());
60-
Language language = languageDomainService.getLanguage(request.languageId());
61-
ProblemInfo problemInfo = problemDomainService.getProblemInfo(problemId);
62-
63-
int passedCount = 0;
64-
String message = COMPILE_MESSAGE;
65-
66-
for (Testcase tc : problemInfo.testcaseList()) {
67-
68-
JudgeResult result = judgeClient.execute(
69-
new CodeCompileRequest(request.sourceCode(), language.getJudge0Id(), tc.getInput())
70-
);
71-
72-
AnswerEvaluation evaluation = submissionDomainService.evaluate(
73-
tc.getOutput(), result.actualOutput(), result.success(), result.executionTime(), result.memoryUsage() , problemInfo
74-
);
75-
76-
if (evaluation.isPassed()) {
77-
passedCount++;
78-
} else {
79-
message = result.message();
80-
}
81-
82-
submissionDomainService.collectStatistics(aggregator, result.executionTime(), result.memoryUsage());
83-
84-
emitter.send(JudgeResultResponse.fromEvaluation(result, evaluation));
85-
}
86-
87-
SubmissionData submissionData = SubmissionData.base(
88-
user, problemInfo, language, request.sourceCode(), message
89-
);
55+
private final JudgeClient judgeClient;
56+
private final ReviewClient reviewClient;
57+
private final UserDomainService userDomainService;
58+
private final ProblemDomainService problemDomainService;
59+
private final LanguageDomainService languageDomainService;
60+
private final SubmissionDomainService submissionDomainService;
61+
private final EmitterStore emitterStore;
62+
private final QueueProducer queueProducer;
63+
private final Executor judgeTestcaseExecutor;
64+
65+
public SseEmitter enqueueCodeSubmission(Long problemId, CodeSubmitRequest request, AuthUser authUser) {
66+
SseEmitter emitter = new SseEmitter(10 * 60 * 1000L);
67+
String emitterKey = authUser.getId() + "_" + UUID.randomUUID();
68+
69+
emitterStore.save(emitterKey, emitter);
70+
71+
SubmissionMessage message = new SubmissionMessage(
72+
emitterKey, problemId, request.languageId(), authUser.getId(), request.sourceCode()
73+
);
74+
75+
queueProducer.enqueue(message);
76+
77+
return emitter;
78+
}
79+
80+
@Async("judgeSubmissionExecutor")
81+
public void submitCodeStream(SubmissionMessage msg) {
82+
try {
83+
log.info("[Submission RUN] Thread = {}", Thread.currentThread().getName());
84+
log.info("Principal = {}", SecurityContextHolder.getContext().getAuthentication());
85+
User user = userDomainService.getUserById(msg.userId());
86+
Language language = languageDomainService.getLanguage(msg.languageId());
87+
ProblemInfo problemInfo = problemDomainService.getProblemInfo(msg.problemId());
88+
SseEmitter emitter = emitterStore.get(msg.emitterKey()).orElseThrow(
89+
() -> new SubmissionException(SubmissionExceptionCode.EMITTER_NOT_FOUND)
90+
);
91+
92+
int totalTestcaseCount = problemInfo.getTestcaseCount();
93+
SubmissionContext context = SubmissionContext.initialize(totalTestcaseCount);
94+
95+
for (Testcase tc : problemInfo.testcaseList()) {
96+
CompletableFuture
97+
.supplyAsync(() -> {
98+
try {
99+
log.info("[Judge RUN] Thread = {}", Thread.currentThread().getName());
100+
log.info("Principal = {}", SecurityContextHolder.getContext().getAuthentication());
101+
String token = judgeClient.submitAndGetToken(
102+
new CodeCompileRequest(msg.sourceCode(), language.getJudge0Id(), tc.getInput())
103+
);
104+
JudgeResult result = judgeClient.pollUntilDone(token);
105+
106+
AnswerEvaluation evaluation = submissionDomainService.handleEvaluationAndUpdateStats(
107+
TestcaseEvaluationInput.from(tc, result), problemInfo, context
108+
);
109+
110+
emitter.send(JudgeResultResponse.fromEvaluation(result, evaluation));
111+
context.countDown();
112+
return result;
113+
} catch (IOException e) {
114+
throw new SubmissionException(SubmissionExceptionCode.EMITTER_SEND_ERROR);
115+
} catch (Exception e) {
116+
throw new SubmissionException(SubmissionExceptionCode.COMPILE_SERVER_ERROR);
117+
}
118+
}, judgeTestcaseExecutor);
119+
}
90120

91-
submissionDomainService.finalizeSubmission(submissionData, aggregator, passedCount);
121+
if (!context.latch().await(30, TimeUnit.SECONDS)) {
122+
throw new SubmissionException(SubmissionExceptionCode.TESTCASE_TIMEOUT);
123+
}
92124

93-
emitter.send(SseEmitter.event()
94-
.name("final")
95-
.data(new FinalResultResponse(problemInfo.getTestcaseCount(), passedCount, message))
96-
);
125+
emitter.send(SseEmitter.event()
126+
.name("final")
127+
.data(context.toFinalResult(totalTestcaseCount)
128+
)
129+
);
97130

98-
emitter.complete();
99-
} catch (Exception e) {
100-
emitter.completeWithError(e);
101-
}
102-
}).start();
131+
emitter.complete();
132+
emitterStore.remove(msg.emitterKey());
103133

104-
return emitter;
105-
}
134+
SubmissionData submissionData = SubmissionData.base(
135+
user, problemInfo, language, msg.sourceCode(), context.getCurrentMessage()
136+
);
106137

107-
@Transactional(readOnly = true)
108-
public List<GroupedSubmissionResponse> getSubmissions(AuthUser authUser) {
138+
submissionDomainService.finalizeSubmission(
139+
submissionData, context.aggregator(), context.getPassedCount()
140+
);
141+
} catch (Exception e) {
142+
emitterStore.get(msg.emitterKey()).ifPresent(emitter -> emitter.completeWithError(e));
143+
}
144+
}
109145

110-
User user = userDomainService.getUserById(authUser.getId());
146+
@Transactional(readOnly = true)
147+
public List<GroupedSubmissionResponse> getSubmissions(AuthUser authUser) {
111148

112-
return submissionDomainService.getSubmissions(user.getId()).stream()
113-
.collect(Collectors.groupingBy(Submission::getProblem))
114-
.entrySet()
115-
.stream()
116-
.map(entry -> {
117-
Problem problem = entry.getKey();
118-
List<Submission> sorted = entry.getValue().stream()
119-
.sorted(Comparator.comparing(Submission::getCreatedAt).reversed())
120-
.toList();
121-
return new GroupedSubmissionResponse(problem, sorted);
122-
})
123-
.toList();
124-
}
149+
User user = userDomainService.getUserById(authUser.getId());
150+
List<Submission> submissions = submissionDomainService.getSubmissions(user.getId());
151+
return GroupedSubmissionResponse.groupByProblem(submissions);
152+
}
125153

126-
public CodeReviewResponse getCodeReview(Long problemId, CodeReviewRequest request) {
154+
public CodeReviewResponse getCodeReview(Long problemId, CodeReviewRequest request) {
127155

128-
Problem problem = problemDomainService.getProblem(problemId);
129-
Language language = languageDomainService.getLanguage(request.languageId());
156+
Problem problem = problemDomainService.getProblem(problemId);
157+
Language language = languageDomainService.getLanguage(request.languageId());
130158

131-
ReviewResult reviewResult = reviewClient.requestReview(ReviewPayload.of(problem, language, request));
159+
ReviewResult reviewResult = reviewClient.requestReview(ReviewPayload.of(problem, language, request));
132160

133-
return new CodeReviewResponse(reviewResult.reviewContent());
134-
}
161+
return new CodeReviewResponse(reviewResult.reviewContent());
162+
}
135163
}

src/main/java/org/ezcode/codetest/common/config/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)