|
1 | 1 | package org.ezcode.codetest.application.submission.service; |
2 | 2 |
|
3 | | -import java.util.Comparator; |
| 3 | +import java.io.IOException; |
4 | 4 | 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; |
6 | 9 |
|
7 | 10 | import org.ezcode.codetest.application.submission.dto.request.review.CodeReviewRequest; |
8 | 11 | import org.ezcode.codetest.application.submission.dto.request.review.ReviewPayload; |
9 | 12 | import org.ezcode.codetest.application.submission.dto.response.review.CodeReviewResponse; |
10 | | -import org.ezcode.codetest.application.submission.dto.response.submission.FinalResultResponse; |
11 | 13 | import org.ezcode.codetest.application.submission.dto.response.submission.GroupedSubmissionResponse; |
| 14 | +import org.ezcode.codetest.application.submission.model.JudgeResult; |
12 | 15 | 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; |
13 | 23 | import org.ezcode.codetest.application.submission.port.ReviewClient; |
14 | 24 | import org.ezcode.codetest.domain.problem.model.ProblemInfo; |
15 | | -import org.ezcode.codetest.domain.submission.model.SubmissionAggregator; |
16 | 25 | import org.ezcode.codetest.domain.submission.dto.AnswerEvaluation; |
17 | 26 | import org.ezcode.codetest.domain.submission.dto.SubmissionData; |
18 | 27 | import org.ezcode.codetest.application.submission.dto.request.compile.CodeCompileRequest; |
19 | 28 | import org.ezcode.codetest.application.submission.dto.request.submission.CodeSubmitRequest; |
20 | | -import org.ezcode.codetest.application.submission.model.JudgeResult; |
21 | 29 | import org.ezcode.codetest.application.submission.dto.response.submission.JudgeResultResponse; |
22 | 30 | import org.ezcode.codetest.application.submission.port.JudgeClient; |
23 | 31 | import org.ezcode.codetest.domain.language.model.entity.Language; |
|
30 | 38 | import org.ezcode.codetest.domain.user.model.entity.AuthUser; |
31 | 39 | import org.ezcode.codetest.domain.user.model.entity.User; |
32 | 40 | import org.ezcode.codetest.domain.user.service.UserDomainService; |
| 41 | +import org.springframework.scheduling.annotation.Async; |
| 42 | +import org.springframework.security.core.context.SecurityContextHolder; |
33 | 43 | import org.springframework.stereotype.Service; |
34 | 44 | import org.springframework.transaction.annotation.Transactional; |
35 | 45 | import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; |
36 | 46 |
|
37 | 47 | import lombok.RequiredArgsConstructor; |
| 48 | +import lombok.extern.slf4j.Slf4j; |
38 | 49 |
|
| 50 | +@Slf4j |
39 | 51 | @Service |
40 | 52 | @RequiredArgsConstructor |
41 | 53 | public class SubmissionService { |
42 | 54 |
|
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 | + } |
90 | 120 |
|
91 | | - submissionDomainService.finalizeSubmission(submissionData, aggregator, passedCount); |
| 121 | + if (!context.latch().await(30, TimeUnit.SECONDS)) { |
| 122 | + throw new SubmissionException(SubmissionExceptionCode.TESTCASE_TIMEOUT); |
| 123 | + } |
92 | 124 |
|
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 | + ); |
97 | 130 |
|
98 | | - emitter.complete(); |
99 | | - } catch (Exception e) { |
100 | | - emitter.completeWithError(e); |
101 | | - } |
102 | | - }).start(); |
| 131 | + emitter.complete(); |
| 132 | + emitterStore.remove(msg.emitterKey()); |
103 | 133 |
|
104 | | - return emitter; |
105 | | - } |
| 134 | + SubmissionData submissionData = SubmissionData.base( |
| 135 | + user, problemInfo, language, msg.sourceCode(), context.getCurrentMessage() |
| 136 | + ); |
106 | 137 |
|
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 | + } |
109 | 145 |
|
110 | | - User user = userDomainService.getUserById(authUser.getId()); |
| 146 | + @Transactional(readOnly = true) |
| 147 | + public List<GroupedSubmissionResponse> getSubmissions(AuthUser authUser) { |
111 | 148 |
|
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 | + } |
125 | 153 |
|
126 | | - public CodeReviewResponse getCodeReview(Long problemId, CodeReviewRequest request) { |
| 154 | + public CodeReviewResponse getCodeReview(Long problemId, CodeReviewRequest request) { |
127 | 155 |
|
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()); |
130 | 158 |
|
131 | | - ReviewResult reviewResult = reviewClient.requestReview(ReviewPayload.of(problem, language, request)); |
| 159 | + ReviewResult reviewResult = reviewClient.requestReview(ReviewPayload.of(problem, language, request)); |
132 | 160 |
|
133 | | - return new CodeReviewResponse(reviewResult.reviewContent()); |
134 | | - } |
| 161 | + return new CodeReviewResponse(reviewResult.reviewContent()); |
| 162 | + } |
135 | 163 | } |
0 commit comments