Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import java.util.List;
import java.util.stream.IntStream;

import org.ezcode.codetest.domain.problem.model.ProblemInfo;
import org.ezcode.codetest.application.submission.model.SubmissionContext;
import org.ezcode.codetest.domain.problem.model.entity.Testcase;

public record InitTestcaseListPayload(
Expand All @@ -17,10 +17,10 @@ public record InitTestcaseListPayload(
String status

) {
public static List<InitTestcaseListPayload> from(ProblemInfo info) {
return IntStream.rangeClosed(1, info.getTestcaseCount())
public static List<InitTestcaseListPayload> from(SubmissionContext ctx) {
return IntStream.rangeClosed(1, ctx.getTestcaseCount())
.mapToObj(seq -> {
Testcase tc = info.testcaseList().get(seq - 1);
Testcase tc = ctx.getTestcases().get(seq - 1);
return new InitTestcaseListPayload(
seq,
tc.getInput(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.ezcode.codetest.application.submission.dto.event.payload;

import org.ezcode.codetest.application.submission.model.JudgeResult;
import org.ezcode.codetest.domain.submission.dto.AnswerEvaluation;

public record TestcaseResultPayload(

Expand All @@ -18,10 +17,10 @@ public record TestcaseResultPayload(
String message

) {
public static TestcaseResultPayload fromEvaluation(int seqId, JudgeResult result, AnswerEvaluation evaluation) {
public static TestcaseResultPayload fromEvaluation(int seqId, boolean isPassed, JudgeResult result) {
return new TestcaseResultPayload(
seqId,
evaluation.isPassed(),
isPassed,
result.actualOutput(),
result.executionTime(),
result.memoryUsage(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package org.ezcode.codetest.application.submission.dto.request.compile;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

import org.ezcode.codetest.application.submission.model.SubmissionContext;

public record CodeCompileRequest(

String source_code,
Expand All @@ -9,4 +14,14 @@ public record CodeCompileRequest(
String stdin

) {
public static CodeCompileRequest of(int seqId, SubmissionContext ctx) {
return new CodeCompileRequest(
ctx.getSourceCode(),
ctx.getJudge0Id(),
ctx.getInput(seqId));
}

private static String encodeBase64(String str) {
return Base64.getEncoder().encodeToString(str.getBytes(StandardCharsets.UTF_8));
}
Comment on lines +24 to +26
Copy link

@coderabbitai coderabbitai bot Jun 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

🧩 Verification correct

Base64 인코딩 메서드가 미사용 상태입니다

encodeBase64 메서드가 정의되어 있지만 같은 클래스의 of 팩토리 메서드에서 사용되지 않고 있습니다. Judge0 API로 전송하는 데이터가 Base64 인코딩이 필요한지 확인이 필요합니다.

Judge0 API 요구사항을 확인하기 위해 다음 스크립트를 실행해주세요:

만약 인코딩이 필요하다면 팩토리 메서드에서 다음과 같이 적용해야 합니다:

 public static CodeCompileRequest of(int seqId, SubmissionContext ctx) {
     return new CodeCompileRequest(
-            ctx.getSourceCode(),
+            encodeBase64(ctx.getSourceCode()),
             ctx.getJudge0Id(),
-            ctx.getInput(seqId));
+            encodeBase64(ctx.getInput(seqId)));
 }

🏁 Script executed:

#!/bin/bash
# Description: CodeCompileRequest에서 Base64 인코딩 사용 여부 및 Judge0Client에서의 처리 확인

# Judge0Client에서 CodeCompileRequest 사용 방식 확인
rg -A 10 -B 5 "CodeCompileRequest" --type java

# Base64 인코딩 관련 코드 검색
rg -A 5 "encodeBase64|Base64" --type java

Length of output: 15868


Base64 인코딩 메서드가 미사용 상태입니다

encodeBase64 메서드가 정의되어 있지만 같은 클래스의 of 팩토리 메서드에서 사용되지 않고 있습니다. Judge0 API로 전송하는 데이터가 Base64 인코딩이 필요한지 확인이 필요합니다.

Judge0 API 요구사항을 확인하기 위해 다음 스크립트를 실행해주세요:

#!/bin/bash
# Description: CodeCompileRequest에서 Base64 인코딩 사용 여부 및 Judge0Client에서의 처리 확인

# Judge0Client에서 CodeCompileRequest 사용 방식 확인
rg -A 10 -B 5 "CodeCompileRequest" --type java

# Base64 인코딩 관련 코드 검색
rg -A 5 "encodeBase64|Base64" --type java

만약 인코딩이 필요하다면 팩토리 메서드에서 다음과 같이 적용해야 합니다:

 public static CodeCompileRequest of(int seqId, SubmissionContext ctx) {
     return new CodeCompileRequest(
-            ctx.getSourceCode(),
+            encodeBase64(ctx.getSourceCode()),
             ctx.getJudge0Id(),
-            ctx.getInput(seqId));
+            encodeBase64(ctx.getInput(seqId)));
 }
🤖 Prompt for AI Agents
In
src/main/java/org/ezcode/codetest/application/submission/dto/request/compile/CodeCompileRequest.java
around lines 24 to 26, the encodeBase64 method is defined but not used anywhere
in the class, including the of factory method. Verify if the Judge0 API requires
the code or input data to be Base64 encoded before sending. If encoding is
required, modify the of factory method to apply encodeBase64 to the relevant
fields before constructing the CodeCompileRequest instance. If encoding is not
needed, consider removing the unused encodeBase64 method to clean up the code.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 이거 관련해서 이슈가 좀 있었어서 이것저것 해보던 건데 안 지웠네 다음 PR에 지울게

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thezz9 네, 이해했습니다! 이슈 해결 과정에서 여러 시도를 해보시느라 남겨둔 코드군요.

혹시 이 정리 작업을 추적하기 위해 follow-up 이슈를 생성해드릴까요? 아니면 다음 PR에서 바로 처리하실 계획이시면 그대로 두셔도 됩니다.

실험적인 코드가 남아있는 것은 충분히 이해할 수 있는 상황입니다! 👍


🧠 Learnings used
Learnt from: thezz9
PR: ezcode-my/backend#36
File: src/main/java/org/ezcode/codetest/application/submission/dto/request/compile/CodeCompileRequest.java:5-9
Timestamp: 2025-06-06T07:52:58.941Z
Learning: CodeCompileRequest 레코드의 필드명(source_code, language_id)은 Java camelCase 컨벤션 대신 snake_case를 사용하는데, 이는 Judge0 API의 반환 값 형식에 맞추기 위한 의도적인 설계 결정이다.

Learnt from: thezz9
PR: ezcode-my/backend#75
File: src/main/java/org/ezcode/codetest/application/submission/aop/CodeReviewLockAspect.java:25-41
Timestamp: 2025-06-19T06:37:57.845Z
Learning: User thezz9 tends to skip validation for scenarios they consider unlikely to occur, but is willing to implement defensive programming practices when suggested for code robustness.

Learnt from: chat26666
PR: ezcode-my/backend#37
File: src/main/java/org/ezcode/codetest/infrastructure/elasticsearch/repository/ProblemElasticsearchRepositoryDsl.java:10-10
Timestamp: 2025-06-06T19:56:27.759Z
Learning: When user chat26666 mentions that code was implemented in a previous PR but explanation was missing, they may submit a follow-up PR just to add documentation/explanation without the actual implementation code.

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ public record ExecutionResultResponse(
ExecutionStatus status

) {
public static ExecutionResultResponse ofCompileError() {
return new ExecutionResultResponse(
null,
0.0,
0L,
null,
null,
"Compilation Error",
1,
new ExecutionResultResponse.ExecutionStatus(6, "Compilation Error")
);
}

public long getMemory() {
return this.memory == null ? 0L : memory;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package org.ezcode.codetest.application.submission.model;

import org.ezcode.codetest.application.submission.dto.event.payload.SubmissionFinalResultPayload;
import org.ezcode.codetest.domain.language.model.entity.Language;
import org.ezcode.codetest.domain.problem.model.ProblemInfo;
import org.ezcode.codetest.domain.problem.model.entity.Problem;
import org.ezcode.codetest.domain.problem.model.entity.Testcase;
import org.ezcode.codetest.domain.submission.model.SubmissionAggregator;
import org.ezcode.codetest.domain.user.model.entity.User;
import org.ezcode.codetest.infrastructure.event.dto.submission.SubmissionMessage;

import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
Expand All @@ -20,48 +27,102 @@ public record SubmissionContext(

CountDownLatch latch,

AtomicBoolean notified
AtomicBoolean notified,

User user,

Language language,

ProblemInfo problemInfo,

SubmissionMessage msg
) {
public static SubmissionContext initialize(int totalTestcaseCount) {
public static SubmissionContext initialize(
User user, Language language, ProblemInfo problemInfo, SubmissionMessage msg
) {
return new SubmissionContext(
new SubmissionAggregator(),
new AtomicInteger(0),
new AtomicInteger(0),
new AtomicReference<>("Accepted"),
new CountDownLatch(totalTestcaseCount),
new AtomicBoolean(false)
new CountDownLatch(problemInfo.getTestcaseCount()),
new AtomicBoolean(false),
user,
language,
problemInfo,
msg
);
}

public SubmissionFinalResultPayload toFinalResult(int totalTestcaseCount) {
public SubmissionFinalResultPayload toFinalResult() {
return new SubmissionFinalResultPayload(
totalTestcaseCount,
this.getTestcaseCount(),
this.getPassedCount(),
this.getCurrentMessage()
);
}

public void incrementPassedCount() {
this.passedCount.incrementAndGet();
passedCount.incrementAndGet();
}

public void incrementProcessedCount() {
this.processedCount.incrementAndGet();
processedCount.incrementAndGet();
}

public int getPassedCount() {
return this.passedCount.get();
return passedCount.get();
}

public String getCurrentMessage() {
return this.message.get();
return message.get();
}

public void updateMessage(String message) {
this.message.set(message);
}

public void countDown() {
this.latch.countDown();
latch.countDown();
}

public List<Testcase> getTestcases() {
return problemInfo.testcaseList();
}

public int getTestcaseCount() {
return problemInfo.getTestcaseCount();
}

public String getSourceCode() {
return msg.sourceCode();
}

public long getJudge0Id() {
return language.getJudge0Id();
}

public String getInput(int seqId) {
return getTestcases().get(seqId - 1).getInput();
}

public String getExpectedOutput(int seqId) {
return getTestcases().get(seqId - 1).getOutput();
}
Comment on lines +105 to +111
Copy link

@coderabbitai coderabbitai bot Jun 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

배열 경계 검사를 추가하는 것이 좋겠습니다.

잘못된 seqId 값이 전달될 경우 IndexOutOfBoundsException이 발생할 수 있습니다.

다음과 같이 경계 검사를 추가하세요:

 public String getInput(int seqId) {
+    if (seqId < 1 || seqId > getTestcaseCount()) {
+        throw new IllegalArgumentException("Invalid seqId: " + seqId);
+    }
     return getTestcases().get(seqId - 1).getInput();
 }

 public String getExpectedOutput(int seqId) {
+    if (seqId < 1 || seqId > getTestcaseCount()) {
+        throw new IllegalArgumentException("Invalid seqId: " + seqId);
+    }
     return getTestcases().get(seqId - 1).getOutput();
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
src/main/java/org/ezcode/codetest/application/submission/model/SubmissionContext.java
around lines 105 to 111, the methods getInput and getExpectedOutput access the
testcases list without checking if seqId is within valid bounds, which can cause
IndexOutOfBoundsException. Add boundary checks to ensure seqId is greater than 0
and less than or equal to the size of the testcases list before accessing it. If
seqId is out of range, handle it appropriately, such as returning null or
throwing a custom exception.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

밖에서 스트림 돌려서 오는 거라 잘못된 값이 전달될 일은 없는데 방어 로직 추가해서 나쁠 건 없다는 말이지?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thezz9 맞습니다! 현재 구현에서는 외부 스트림에서 순차적으로 오는 값이라 잘못된 seqId가 전달될 가능성이 낮지만, 방어 로직을 추가하는 것이 여러 면에서 도움이 됩니다:

방어 로직의 장점:

  • 미래 변경사항 대비: 코드가 수정되면서 호출 패턴이 바뀔 수 있음
  • 디버깅 편의성: 문제 발생 시 명확한 에러 메시지로 원인 파악이 쉬움
  • 코드 자체 문서화: 메서드가 기대하는 입력 범위를 명시적으로 표현
  • Fail-fast 원칙: 문제를 빨리 발견하고 처리

특히 현재 PR에서 코드 구조를 개선하고 예외 처리를 강화하는 작업을 하고 계시니, 이런 방어 로직도 전체적인 코드 견고성 향상에 부합하는 것 같습니다.

추가하실지는 전체적인 코드 정책과 우선순위에 따라 결정하시면 될 것 같습니다!


🧠 Learnings used
Learnt from: thezz9
PR: ezcode-my/backend#30
File: src/main/java/org/ezcode/codetest/application/submission/service/SubmissionService.java:44-46
Timestamp: 2025-06-04T13:12:17.636Z
Learning: SubmissionService에서 "2 10" 입력값과 "12" 기대출력이 하드코딩되어 있는 것은 테스트 단계의 의도적인 구현이며, 추후 Problem 엔티티의 실제 테스트케이스로 대체 예정입니다.

Learnt from: thezz9
PR: ezcode-my/backend#75
File: src/main/java/org/ezcode/codetest/application/submission/aop/CodeReviewLockAspect.java:25-41
Timestamp: 2025-06-19T06:37:57.845Z
Learning: User thezz9 tends to skip validation for scenarios they consider unlikely to occur, but is willing to implement defensive programming practices when suggested for code robustness.

Learnt from: chat26666
PR: ezcode-my/backend#37
File: src/main/java/org/ezcode/codetest/infrastructure/elasticsearch/repository/ProblemElasticsearchRepositoryDsl.java:10-10
Timestamp: 2025-06-06T19:56:27.759Z
Learning: When user chat26666 mentions that code was implemented in a previous PR but explanation was missing, they may submit a follow-up PR just to add documentation/explanation without the actual implementation code.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thezz9 알겠습니다! 필요하시면 언제든 말씀해 주세요. 현재 PR의 예외 처리 강화 작업 잘 진행되고 있는 것 같습니다. 👍


🧠 Learnings used
Learnt from: thezz9
PR: ezcode-my/backend#75
File: src/main/java/org/ezcode/codetest/application/submission/aop/CodeReviewLockAspect.java:25-41
Timestamp: 2025-06-19T06:37:57.845Z
Learning: User thezz9 tends to skip validation for scenarios they consider unlikely to occur, but is willing to implement defensive programming practices when suggested for code robustness.

Learnt from: thezz9
PR: ezcode-my/backend#30
File: src/main/java/org/ezcode/codetest/application/submission/service/SubmissionService.java:44-46
Timestamp: 2025-06-04T13:12:17.636Z
Learning: SubmissionService에서 "2 10" 입력값과 "12" 기대출력이 하드코딩되어 있는 것은 테스트 단계의 의도적인 구현이며, 추후 Problem 엔티티의 실제 테스트케이스로 대체 예정입니다.

Learnt from: thezz9
PR: ezcode-my/backend#30
File: src/main/java/org/ezcode/codetest/domain/problem/model/dto/SubmissionData.java:42-53
Timestamp: 2025-06-04T13:12:16.729Z
Learning: 사용자 thezz9는 개발 시 기능적 정확성을 먼저 달성한 후 세부 리팩터링을 나중에 하는 접근 방식을 선호한다. 테스트 단계에서는 하드코딩된 값도 허용한다.

Learnt from: chat26666
PR: ezcode-my/backend#37
File: src/main/java/org/ezcode/codetest/infrastructure/elasticsearch/repository/ProblemElasticsearchRepositoryDsl.java:10-10
Timestamp: 2025-06-06T19:56:27.759Z
Learning: When user chat26666 mentions that code was implemented in a previous PR but explanation was missing, they may submit a follow-up PR just to add documentation/explanation without the actual implementation code.


public long getTimeLimit() {
return problemInfo.getTimeLimit();
}

public long getMemoryLimit() {
return problemInfo.getMemoryLimit();
}

public String getSessionKey() {
return msg.sessionKey();
}

public Problem getProblem() {
return problemInfo.problem();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

public interface ExceptionNotifier {

void sendEmbed(String title, String description, String exception, String methodName);
void notifyException(String methodName, Throwable t);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package org.ezcode.codetest.application.submission.service;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

import org.ezcode.codetest.application.submission.dto.event.SubmissionErrorEvent;
import org.ezcode.codetest.application.submission.dto.event.SubmissionJudgingFinishedEvent;
import org.ezcode.codetest.application.submission.dto.event.TestcaseEvaluatedEvent;
import org.ezcode.codetest.application.submission.dto.event.TestcaseListInitializedEvent;
import org.ezcode.codetest.application.submission.dto.event.payload.InitTestcaseListPayload;
import org.ezcode.codetest.application.submission.dto.event.payload.TestcaseResultPayload;
import org.ezcode.codetest.application.submission.dto.request.compile.CodeCompileRequest;
import org.ezcode.codetest.application.submission.model.JudgeResult;
import org.ezcode.codetest.application.submission.model.SubmissionContext;
import org.ezcode.codetest.application.submission.port.ExceptionNotifier;
import org.ezcode.codetest.application.submission.port.JudgeClient;
import org.ezcode.codetest.application.submission.port.ProblemEventService;
import org.ezcode.codetest.application.submission.port.SubmissionEventService;
import org.ezcode.codetest.domain.submission.exception.SubmissionException;
import org.ezcode.codetest.domain.submission.exception.code.SubmissionExceptionCode;
import org.ezcode.codetest.domain.submission.model.SubmissionResult;
import org.ezcode.codetest.domain.submission.model.TestcaseEvaluationInput;
import org.ezcode.codetest.domain.submission.service.SubmissionDomainService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor
public class JudgementService {

private final SubmissionDomainService submissionDomainService;
private final SubmissionEventService submissionEventService;
private final ProblemEventService problemEventService;
private final JudgeClient judgeClient;
private final Executor judgeTestcaseExecutor;
private final ExceptionNotifier exceptionNotifier;

public void publishInitTestcases(SubmissionContext ctx) {
submissionEventService.publishInitTestcases(
new TestcaseListInitializedEvent(ctx.getSessionKey(), InitTestcaseListPayload.from(ctx))
);
}

public void runTestcases(SubmissionContext ctx) throws InterruptedException {
IntStream.rangeClosed(1, ctx.getTestcaseCount())
.forEach(seqId -> runTestcaseAsync(seqId, ctx));

if (!ctx.latch().await(100, TimeUnit.SECONDS)) {
throw new SubmissionException(SubmissionExceptionCode.TESTCASE_TIMEOUT);
}
}

@Transactional
public void finalizeAndPublish(SubmissionContext ctx) {
SubmissionResult submissionResult = submissionDomainService.finalizeSubmission(ctx);

publishFinalResult(ctx);
publishProblemSolve(submissionResult);
}

public void publishSubmissionError(String sessionKey, Exception e) {
submissionEventService.publishSubmissionError(new SubmissionErrorEvent(sessionKey, e));
}

private void runTestcaseAsync(int seqId, SubmissionContext ctx) {
CompletableFuture.runAsync(() -> {
try {
log.info("[Judge RUN] Thread = {}", Thread.currentThread().getName());
String token = judgeClient.submitAndGetToken(CodeCompileRequest.of(seqId, ctx));
JudgeResult result = judgeClient.pollUntilDone(token);

TestcaseEvaluationInput input = TestcaseEvaluationInput.from(result, ctx, seqId);

boolean isPassed = submissionDomainService.handleEvaluationAndUpdateStats(input, ctx);

publishTestcaseUpdate(seqId, ctx, isPassed, result);
} catch (Exception e) {
if (ctx.notified().compareAndSet(false, true)) {
publishSubmissionError(ctx.getSessionKey(), e);
exceptionNotifier.notifyException("runTestcaseAsync", e);
}
} finally {
ctx.countDown();
}
}, judgeTestcaseExecutor);
}

private void publishTestcaseUpdate(int seqId, SubmissionContext ctx, boolean isPassed, JudgeResult result) {
submissionEventService.publishTestcaseUpdate(new TestcaseEvaluatedEvent(
ctx.getSessionKey(), TestcaseResultPayload.fromEvaluation(seqId, isPassed, result))
);
}

private void publishFinalResult(SubmissionContext ctx){
submissionEventService.publishFinalResult(
new SubmissionJudgingFinishedEvent(ctx.getSessionKey(), ctx.toFinalResult())
);
}

private void publishProblemSolve(SubmissionResult submissionResult) {
problemEventService.publishProblemSolveEvent(submissionResult);
}
}
Loading