-
Notifications
You must be signed in to change notification settings - Fork 3
test(submission): add infra layer tests #120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Walkthrough
Changes
Sequence Diagram(s)sequenceDiagram
participant Test as TestClass
participant RedisConsumer as RedisJudgeQueueConsumer
participant SubmissionService
participant Redis as Redis Stream
Test->>RedisConsumer: onMessage(수신)
RedisConsumer->>SubmissionService: processSubmissionAsync(SubmissionMessage)
alt 성공
SubmissionService-->>RedisConsumer: 완료
RedisConsumer->>Redis: acknowledge(메시지)
else 실패
SubmissionService-->>RedisConsumer: 예외 발생
RedisConsumer--x Redis: (acknowledge 호출 안함)
end
sequenceDiagram
participant Test as TestClass
participant Judge0Client
participant MockWebServer
Test->>Judge0Client: submitAndGetToken(요청)
Judge0Client->>MockWebServer: POST /submissions
MockWebServer-->>Judge0Client: 응답(성공/실패/지연)
alt 성공
Judge0Client-->>Test: token 반환
else 서버오류
Judge0Client-->>Test: SubmissionException(COMPILE_SERVER_ERROR)
else 타임아웃
Judge0Client-->>Test: SubmissionException(COMPILE_TIMEOUT)
end
Suggested reviewers
Poem
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (2)
src/test/java/org/ezcode/codetest/infrastructure/github/GitHubClientTest.java (1)
48-121: 추가 테스트 케이스 고려를 제안합니다.현재 테스트는 SHA 비교 로직의 주요 경로를 잘 커버하고 있습니다. 향후 다음과 같은 엣지 케이스도 추가를 고려해보세요:
- fetchSourceBlobSha가 Optional.empty()를 반환하는 경우
- API 호출 중 예외가 발생하는 경우
src/test/java/org/ezcode/codetest/infrastructure/judge0/Judge0ClientTest.java (1)
180-193: 타임아웃 테스트의 실제 동작 시간 확인 필요이 테스트는 65개의 응답을 큐에 넣어 60초 타임아웃을 테스트하려고 하지만,
pollUntilDone메서드는Flux.interval을 사용하므로 실제로 60초가 걸릴 수 있습니다. 테스트 실행 시간이 너무 길어지지 않는지 확인이 필요합니다.시간 기반 테스트를 위해
StepVerifier와 가상 시간을 사용하는 것을 고려해보세요:+import reactor.test.StepVerifier; +import reactor.test.scheduler.VirtualTimeScheduler;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
build.gradle(2 hunks)src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0Client.java(2 hunks)src/test/java/org/ezcode/codetest/infrastructure/event/RedisJudgeQueueConsumerTest.java(1 hunks)src/test/java/org/ezcode/codetest/infrastructure/event/RedisJudgeQueueProducerTest.java(1 hunks)src/test/java/org/ezcode/codetest/infrastructure/github/GitHubClientTest.java(1 hunks)src/test/java/org/ezcode/codetest/infrastructure/judge0/Judge0ClientTest.java(1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
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#114
File: src/main/java/org/ezcode/codetest/infrastructure/github/GitHubApiClient.java:93-96
Timestamp: 2025-06-28T02:04:24.910Z
Learning: User thezz9 sometimes temporarily uses less secure or inconsistent exception handling (like RuntimeException with raw error bodies) for debugging purposes when error messages are being hidden in the normal application flow, but acknowledges this as technical debt and fixes it once debugging is complete.
Learnt from: thezz9
PR: ezcode-my/backend#114
File: src/main/java/org/ezcode/codetest/infrastructure/github/GitHubApiClient.java:159-165
Timestamp: 2025-06-28T02:03:38.654Z
Learning: User thezz9's service (ezcode) targets Korean users exclusively ("찐 한국인밖에 안 옴"), so Korean language hardcoding in commit messages and UI elements is intentional and appropriate for their use case.
src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0Client.java (2)
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#98
File: src/main/java/org/ezcode/codetest/application/submission/dto/event/SubmissionErrorEvent.java:17-22
Timestamp: 2025-06-25T01:24:30.754Z
Learning: JudgeClient에서 모든 예외를 SubmissionException으로 래핑해서 넘길 계획이므로, SubmissionErrorEvent.resolveCode() 메서드의 캐스팅은 안전할 예정임.
src/test/java/org/ezcode/codetest/infrastructure/event/RedisJudgeQueueConsumerTest.java (2)
Learnt from: thezz9
PR: ezcode-my/backend#68
File: src/main/java/org/ezcode/codetest/infrastructure/event/listener/RedisJudgeQueueConsumer.java:43-45
Timestamp: 2025-06-17T02:49:32.334Z
Learning: Spring Data Redis의 StreamOperations.acknowledge() 메서드는 acknowledge(String group, MapRecord... records) 시그니처를 지원하며, Consumer 객체 없이도 그룹명과 메시지 레코드만으로 ACK 처리가 가능합니다.
Learnt from: thezz9
PR: ezcode-my/backend#98
File: src/main/java/org/ezcode/codetest/application/submission/dto/event/SubmissionErrorEvent.java:17-22
Timestamp: 2025-06-25T01:24:30.754Z
Learning: JudgeClient에서 모든 예외를 SubmissionException으로 래핑해서 넘길 계획이므로, SubmissionErrorEvent.resolveCode() 메서드의 캐스팅은 안전할 예정임.
src/test/java/org/ezcode/codetest/infrastructure/event/RedisJudgeQueueProducerTest.java (1)
Learnt from: thezz9
PR: ezcode-my/backend#68
File: src/main/java/org/ezcode/codetest/infrastructure/event/listener/RedisJudgeQueueConsumer.java:43-45
Timestamp: 2025-06-17T02:49:32.334Z
Learning: Spring Data Redis의 StreamOperations.acknowledge() 메서드는 acknowledge(String group, MapRecord... records) 시그니처를 지원하며, Consumer 객체 없이도 그룹명과 메시지 레코드만으로 ACK 처리가 가능합니다.
src/test/java/org/ezcode/codetest/infrastructure/judge0/Judge0ClientTest.java (3)
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#73
File: src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIReviewClient.java:52-64
Timestamp: 2025-06-18T11:42:03.701Z
Learning: OpenAI API 클라이언트에서는 두 가지 재시도 전략을 사용한다: 1) WebClient의 retryWhen은 네트워크/HTTP 에러 및 타임아웃에 대한 재시도, 2) 외부 for 루프는 API 응답 포맷 검증 실패에 대한 재시도. 이는 서로 다른 실패 상황을 처리하는 독립적인 메커니즘이다.
Learnt from: thezz9
PR: ezcode-my/backend#98
File: src/main/java/org/ezcode/codetest/application/submission/dto/event/SubmissionErrorEvent.java:17-22
Timestamp: 2025-06-25T01:24:30.754Z
Learning: JudgeClient에서 모든 예외를 SubmissionException으로 래핑해서 넘길 계획이므로, SubmissionErrorEvent.resolveCode() 메서드의 캐스팅은 안전할 예정임.
🧬 Code Graph Analysis (1)
src/test/java/org/ezcode/codetest/infrastructure/event/RedisJudgeQueueProducerTest.java (1)
src/test/java/org/ezcode/codetest/infrastructure/event/RedisJudgeQueueConsumerTest.java (1)
ExtendWith(27-103)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: build
🔇 Additional comments (12)
src/test/java/org/ezcode/codetest/infrastructure/github/GitHubClientTest.java (3)
1-46: 테스트 클래스 구조가 잘 설계되었습니다.Mockito와 JUnit 5를 활용한 표준적인 테스트 구조로 의존성 주입과 모킹이 적절히 설정되었습니다. 한국어 DisplayName 사용도 팀의 코딩 가이드라인에 부합합니다.
48-66: 첫 번째 테스트 시나리오가 정확합니다.블롭 SHA가 변경되지 않았을 때의 얼리 리턴 로직을 정확히 테스트하고 있으며, 불필요한 API 호출이 발생하지 않는다는 것을 올바르게 검증하고 있습니다.
68-94: 두 번째 테스트 시나리오도 적절합니다.새 블롭 SHA가 트리 SHA와 동일할 때의 얼리 리턴 로직을 테스트하여, 트리 생성까지는 진행하지만 커밋은 하지 않는 시나리오를 잘 검증하고 있습니다.
src/test/java/org/ezcode/codetest/infrastructure/event/RedisJudgeQueueProducerTest.java (2)
40-71: 잘 구성된 테스트입니다!enqueue 기능에 대한 테스트가 포괄적으로 작성되었습니다:
- 모든 메시지 필드가 올바르게 문자열로 변환되는지 검증
- ArgumentCaptor를 사용한 정확한 인자 캡처
- assertAll을 통한 명확한 다중 어설션
30-33: 제네릭 호환성 문제 없음
opsForStream()는 호출 시점에 HK, HV 타입을 지정하는 제네릭 메소드(<HK, HV> StreamOperations<K, HK, HV>)이므로,
RedisTemplate<String, String>과StreamOperations<String, Object, Object>간 제네릭 타입 충돌이 없습니다.
테스트는 정상 컴파일 및 실행됩니다.src/test/java/org/ezcode/codetest/infrastructure/event/RedisJudgeQueueConsumerTest.java (3)
46-59: 테스트 데이터 설정이 잘 되어 있습니다.BeforeEach에서 Redis 레코드 모킹이 적절하게 구성되었습니다. 모든 필수 필드가 포함되어 있고 RecordId도 올바르게 설정되었습니다.
61-86: 성공 시나리오 테스트가 포괄적입니다.정상 처리 흐름에 대한 테스트가 잘 작성되었습니다:
- ArgumentCaptor로 SubmissionMessage 검증
- 모든 필드의 타입 변환 확인 (String → Long)
- Redis 스트림 acknowledgment 호출 검증
이전 학습 내용에 따라
acknowledge(String group, MapRecord... records)시그니처 사용이 올바릅니다.
88-102: 예외 처리 시나리오가 적절합니다.실패 케이스에 대한 테스트도 잘 구성되었습니다:
- RuntimeException 발생시 SubmissionException으로 래핑 확인
- 적절한 에러 메시지 검증
- 실패시 acknowledgment 미호출 확인
에러 처리 로직이 정확하게 검증되고 있습니다.
build.gradle (2)
67-67: LGTM!테스트를 위한 MockWebServer 종속성 추가가 적절합니다.
27-28: 중복 종속성 확인 필요
spring-boot-starter-webflux(build.gradle:35)가 이미reactor-core와reactor-netty를 트랜짓 의존성으로 포함하고 있을 가능성이 높습니다. 아래 위치를 로컬에서 Gradle 종속성 트리를 통해 직접 확인한 뒤, 중복이라면 명시적 선언을 제거하세요.
- build.gradle 27–28:
implementation 'io.projectreactor:reactor-core'
implementation 'io.projectreactor.netty:reactor-netty'- build.gradle 35:
implementation 'org.springframework.boot:spring-boot-starter-webflux'로컬에서 다음과 같은 명령으로 검증해 보세요:
./gradlew dependencyInsight --configuration compileClasspath --dependency reactor-core ./gradlew dependencyInsight --configuration compileClasspath --dependency reactor-nettysrc/test/java/org/ezcode/codetest/infrastructure/judge0/Judge0ClientTest.java (2)
58-74: 테스트용 WebClient 필터 설정의 의도 확인테스트 설정에서 WebClient에 커스텀 필터를 추가하여 5xx 에러와 TimeoutException을 SubmissionException으로 변환하고 있습니다. 이는 실제 구현과 다른 동작을 할 수 있어 보입니다. 실제 Judge0Client의 에러 핸들링 로직을 정확히 테스트하려면 이러한 필터 없이 MockWebServer의 응답만으로 테스트하는 것이 더 적절할 수 있습니다.
81-195: 테스트 구조와 커버리지가 우수합니다!성공 케이스와 실패 케이스가 체계적으로 구성되어 있고, ArgumentCaptor를 사용한 상세한 검증이 잘 되어 있습니다. Judge0Client의 주요 시나리오들이 모두 커버되어 있습니다.
| // given | ||
| String oldSha = "old"; | ||
| String newSha = "new"; | ||
| given(blobCalculator.calculateBlobSha(request.sourceCode())).willReturn(newSha); | ||
| given(gitHubApiClient.fetchSourceBlobSha(request)).willReturn(Optional.of(oldSha)); | ||
| given(gitHubApiClient.fetchCommitContext(request)).willReturn(ctx); | ||
|
|
||
| List<Map<String,Object>> entries = List.of(Map.of()); | ||
| given(templateBuilder.buildGitTreeEntries(request, newSha)).willReturn(entries); | ||
|
|
||
| given(ctx.baseTreeSha()).willReturn("tree"); | ||
| given(ctx.headCommitSha()).willReturn("head"); | ||
| String newTree = "newTree"; | ||
| given(gitHubApiClient.createTree(request, "tree", entries)).willReturn(newTree); | ||
|
|
||
| // when | ||
| gitHubClientImpl.commitAndPushToRepo(request); | ||
|
|
||
| // then | ||
| then(gitHubApiClient).should().commitAndPush(request, "head", newTree); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
모킹 설정의 일관성을 개선해주세요.
세 번째 테스트에서 request.sourceCode() 호출에 대한 모킹이 누락되었습니다. 다른 테스트들과 일관성을 위해 이 설정을 추가해야 합니다.
다음 diff를 적용하여 모킹 설정을 완성해주세요:
// given
String oldSha = "old";
String newSha = "new";
+ given(request.sourceCode()).willReturn(sourceCode);
given(blobCalculator.calculateBlobSha(request.sourceCode())).willReturn(newSha);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // given | |
| String oldSha = "old"; | |
| String newSha = "new"; | |
| given(blobCalculator.calculateBlobSha(request.sourceCode())).willReturn(newSha); | |
| given(gitHubApiClient.fetchSourceBlobSha(request)).willReturn(Optional.of(oldSha)); | |
| given(gitHubApiClient.fetchCommitContext(request)).willReturn(ctx); | |
| List<Map<String,Object>> entries = List.of(Map.of()); | |
| given(templateBuilder.buildGitTreeEntries(request, newSha)).willReturn(entries); | |
| given(ctx.baseTreeSha()).willReturn("tree"); | |
| given(ctx.headCommitSha()).willReturn("head"); | |
| String newTree = "newTree"; | |
| given(gitHubApiClient.createTree(request, "tree", entries)).willReturn(newTree); | |
| // when | |
| gitHubClientImpl.commitAndPushToRepo(request); | |
| // then | |
| then(gitHubApiClient).should().commitAndPush(request, "head", newTree); | |
| } | |
| // given | |
| String oldSha = "old"; | |
| String newSha = "new"; | |
| given(request.sourceCode()).willReturn(sourceCode); | |
| given(blobCalculator.calculateBlobSha(request.sourceCode())).willReturn(newSha); | |
| given(gitHubApiClient.fetchSourceBlobSha(request)).willReturn(Optional.of(oldSha)); | |
| given(gitHubApiClient.fetchCommitContext(request)).willReturn(ctx); | |
| List<Map<String,Object>> entries = List.of(Map.of()); | |
| given(templateBuilder.buildGitTreeEntries(request, newSha)).willReturn(entries); | |
| given(ctx.baseTreeSha()).willReturn("tree"); | |
| given(ctx.headCommitSha()).willReturn("head"); | |
| String newTree = "newTree"; | |
| given(gitHubApiClient.createTree(request, "tree", entries)).willReturn(newTree); | |
| // when | |
| gitHubClientImpl.commitAndPushToRepo(request); | |
| // then | |
| then(gitHubApiClient).should().commitAndPush(request, "head", newTree); |
🤖 Prompt for AI Agents
In src/test/java/org/ezcode/codetest/infrastructure/github/GitHubClientTest.java
around lines 100 to 120, the mocking setup is missing a stub for the call to
request.sourceCode(), which is used in the third test. To fix this, add a
mocking setup for request.sourceCode() to return the expected source code
string, ensuring consistency with other tests that mock this method call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
어라 빼먹어도 잘 돌아가던데...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@thezz9 맞습니다, 테스트가 실제로 통과할 수 있어요. 아마도 Mockito가 모킹되지 않은 메서드 호출에 대해 기본값(null 등)을 반환하거나, blobCalculator.calculateBlobSha()가 null 값을 적절히 처리하고 있을 가능성이 높습니다.
하지만 일관된 모킹을 유지하는 것이 좋은 이유는:
- 명시적 계약: 테스트에서 어떤 값들이 사용되는지 명확하게 보여줍니다
- 테스트 안정성: 구현체 변경 시에도 테스트가 예측 가능하게 동작합니다
- 가독성: 다른 개발자가 봤을 때 모든 의존성이 명확히 드러납니다
현재는 우연히 동작하는 상황이지만, 나중에 calculateBlobSha 구현이 바뀌면 테스트가 깨질 수 있어요. 하지만 기능적으로 잘 동작한다면 굳이 지금 당장 바꿀 필요는 없습니다! 🙂
🧠 Learnings used
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: thezz9
PR: ezcode-my/backend#114
File: src/main/java/org/ezcode/codetest/infrastructure/github/GitHubApiClient.java:159-165
Timestamp: 2025-06-28T02:03:38.654Z
Learning: User thezz9's service (ezcode) targets Korean users exclusively ("찐 한국인밖에 안 옴"), so Korean language hardcoding in commit messages and UI elements is intentional and appropriate for their use case.
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#114
File: src/main/java/org/ezcode/codetest/infrastructure/github/GitHubApiClient.java:93-96
Timestamp: 2025-06-28T02:04:24.910Z
Learning: User thezz9 sometimes temporarily uses less secure or inconsistent exception handling (like RuntimeException with raw error bodies) for debugging purposes when error messages are being hidden in the normal application flow, but acknowledges this as technical debt and fixes it once debugging is complete.
| .onErrorMap(IllegalStateException.class, | ||
| ex -> new SubmissionException(SubmissionExceptionCode.COMPILE_TIMEOUT)) | ||
| .onErrorMap(WebClientResponseException.class, | ||
| ex -> new SubmissionException(SubmissionExceptionCode.COMPILE_SERVER_ERROR)) | ||
| .onErrorMap(TimeoutException.class, | ||
| ex -> new SubmissionException(SubmissionExceptionCode.COMPILE_TIMEOUT)) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
IllegalStateException 매핑의 근거 확인 필요
IllegalStateException이 COMPILE_TIMEOUT으로 매핑되는 것이 추가되었습니다. WebClient가 어떤 상황에서 이 예외를 던지는지 명확히 문서화하거나 주석을 추가하면 좋겠습니다.
WebClient가 IllegalStateException을 던지는 시나리오를 확인해주세요:
🌐 Web query:
When does Spring WebFlux WebClient throw IllegalStateException?
💡 Result:
Spring WebFlux’s WebClient can throw a java.lang.IllegalStateException in a few key scenarios. The two main cases supported by your search results are:
1. Blocking Calls Inside Non-blocking (Reactive) Threads
- Cause:
You callblock(),blockFirst(), orblockLast()on a reactive publisher (like aMonoorFlux) returned by WebClient, but do so within a thread handled by the Reactor Netty event loop (such asreactor-http-nio-*orreactor-http-epoll-*). These threads must remain non-blocking for the scalability of the reactive model. - Exception message:
java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-4 - Why:
The Reactor framework enforces non-blocking behavior; blocking calls in these threads can stall the entire event loop, hurting performance and defeating the purpose of using reactive programming. - Prevention:
Use only non-blocking operations. If you must block (e.g., for legacy interop), switch to a thread designed for blocking (such as those fromSchedulers.boundedElastic()), never the event loop threads[1][4][6].
It means that you are executing this code in a thread which can not be blocked, in your case reactor-http-nio-4. If you run this code in main thread, or any other, it will work[6].
2. Only One Subscriber for Response Body
- Cause:
IllegalStateException: Only one connection receive subscriber allowedis thrown if you try to subscribe more than once to the same response body, such as with a POST request using certain content types (e.g.,application/x-www-form-urlencoded), or by calling a terminal operation on the same response multiple times. - Why:
The HTTP response body in WebClient can only be read once. Attempting a second subscription is an illegal state[7].
3. Decoder Not Found for Response Type
- Cause:
WebClient throwsIllegalStateExceptionif it cannot find a suitable decoder to convert the HTTP response into the requested Java type. - When:
If you attempt to extract a response body as a specific object or data structure but no compatible decoder is registered, this exception is thrown[2].
IllegalStateException - if a Decoder cannot be found[2].
Summary Table
| Scenario | When it happens | Sample Exception Message |
|---|---|---|
| Blocking a reactive thread (e.g. event loop) | Calling block() or similar on a Netty/reactor event loop thread |
IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread ... |
| Subscribing more than once to a response body | Reading/subscribing to a WebClient response body more than once | IllegalStateException: Only one connection receive subscriber allowed |
| No decoder found for expected response type | No suitable codec to deserialize to the required Java type | IllegalStateException: No suitable decoder found |
Best Practices:
- Never block in a reactive thread. Offload blocking calls to dedicated thread pools like
boundedElastic[1][4][6]. - Read response bodies once.
- Ensure response codecs are available for all expected target types.
If you experience IllegalStateException with WebClient, first check if any of these conditions are true in your application logic.
Citations:
- 1: https://www.javacodegeeks.com/fix-illegalstateexception-blocking.html
- 2: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/reactive/function/client/WebClientResponseException.html
- 3: https://www.baeldung.com/spring-webflux-errors
- 4: https://www.baeldung.com/java-fix-illegalstateexception-blocking
- 5: https://community.atlassian.com/forums/Jira-questions/ClassNotFoundException-org-springframework-web-reactive-function/qaq-p/2502220
- 6: WebClient.block throws java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread spring-projects/spring-framework#22919
- 7: WebClient throws "java.lang.IllegalStateException: Only one connection receive subscriber allowed" on Post Method with content-type x-www-form-urlencoded spring-projects/spring-framework#22284
- 8: https://devforum.okta.com/t/cannot-connect-to-issuer-uri-using-spring-boot-application/6851
- 9: https://dzone.com/articles/error-handling-in-spring-webflux
- 10: https://community.atlassian.com/t5/Jira-questions/ClassNotFoundException-org-springframework-web-reactive-function/qaq-p/2502220
IllegalStateException 매핑 재검토 및 구체화 필요
WebClient가 던지는 IllegalStateException은 다음과 같이 다양한 원인으로 발생합니다.
- 블록 호출(block()/blockFirst()/blockLast()) 시 비동기 Reactor 스레드에서 발생
- 동일 응답 바디에 대한 다중 구독 시 발생
- 요청한 타입에 대한 디코더 미발견 시 발생
위 모든 경우를 COMPILE_TIMEOUT으로 매핑하면 실제 컴파일 타임아웃 외의 이슈가 가려질 수 있습니다.
따라서 아래 중 하나를 적용해 매핑 조건을 구체화하세요:
ex.getMessage()에"block()/blockFirst()/blockLast()"문구가 포함된 경우만COMPILE_TIMEOUT으로 매핑- 매핑 근거를 주석으로 명시
- 불필요한
IllegalStateException매핑 제거
대상 위치:
- 파일: src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0Client.java
- 라인: 58–64
🤖 Prompt for AI Agents
In src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0Client.java
lines 58 to 64, the current mapping of all IllegalStateException instances to
COMPILE_TIMEOUT is too broad and may mask other issues. Refine the mapping by
checking if ex.getMessage() contains "block()/blockFirst()/blockLast()" and only
then map to COMPILE_TIMEOUT, otherwise do not map or remove the general
IllegalStateException mapping. Add a comment explaining this conditional mapping
to clarify the rationale.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RetryExhaustedException으로 넘어오는데 이게 package-private 클래스라 import 할 수가 없어서 부모 클래스인 IllegalStateException 를 쓴 거야.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
@thezz9 완전히 이해했습니다! RetryExhaustedException이 package-private 클래스라서 직접 import할 수 없어서 부모 클래스인 IllegalStateException를 사용하신 거군요.
이는 Reactor 라이브러리의 API 설계 제한으로 인한 합리적인 해결책입니다. 다만 이런 배경을 모르는 다른 개발자들을 위해 주석으로 설명을 추가하시면 좋을 것 같습니다:
// RetryExhaustedException is package-private, so catch its parent IllegalStateException
.onErrorMap(IllegalStateException.class,
ex -> new SubmissionException(SubmissionExceptionCode.COMPILE_TIMEOUT))이전 리뷰 코멘트는 철회하겠습니다. 감사합니다! 🙏
🌐 Web query:
Reactor RetryExhaustedException package-private IllegalStateException parent class
💡 Result:
The RetryExhaustedException is a class that is often found in retry mechanism libraries or frameworks, such as those used in networking or task execution. You are asking about its package-private parent class, which is IllegalStateException.
However, IllegalStateException itself is a public class in Java and not package-private. It is part of the java.lang package and is a subclass of RuntimeException.
If RetryExhaustedException has a package-private parent class (a class with no public or protected modifier, thus only accessible within its own package), that means there is a custom exception class that RetryExhaustedException extends, which itself might extend IllegalStateException.
Could you please specify the library or framework you are referring to or provide more context? This will help me provide a more accurate explanation or code example.
🌐 Web query:
Project Reactor retryWhen RetryExhaustedException package private visibility
💡 Result:
When using Project Reactor's retryWhen operator, if all retries are exhausted, the framework throws a RetryExhaustedException that wraps the last error encountered during the retry sequence[1][3][4]. This exception helps you programmatically distinguish between a failure due to retry exhaustion and other types of errors.
Package-private Visibility of RetryExhaustedException
- In reactor-core, the exception thrown after retries are exhausted is typically a
RetryExhaustedException(sometimes known in older versions asRetry.RetryExhaustedException). - In
reactor-extra,RetryExhaustedExceptionis a public class extendingRuntimeException[10]. Its constructors are all public, which means you can catch and use this exception freely if you have direct access to the reactor-extra dependency. - However, in reactor-core, documentation and common API usage do not directly expose a
RetryExhaustedExceptionas a public class in the core package documentation[9]. Instead, if you useretryWhenwith aRetryspec and do not customize the exhaustion behavior, the framework will throw a package-private exception internally. This means:- The exception class may not be directly catchable outside the
reactor.util.retry(or internal) package. - This limits your ability to do fine-grained exception handling on that specific type unless you override the
onRetryExhaustedThrowmethod to throw your own exception[1][3][7].
- The exception class may not be directly catchable outside the
Why package-private?
- Package-private (default) visibility in Java means a class or member is only accessible from within its own package and not from outside[2]. This is often chosen for internal exception types in libraries when the authors want to avoid exposing implementation details as part of the public API.
What to do if you need custom handling?
- Override the exhaustion behavior using
onRetryExhaustedThrow, supplying your own (public) exception type[1][3][7]:
.retryWhen(
Retry.backoff(3, Duration.ofSeconds(5))
.onRetryExhaustedThrow((retrySpec, retrySignal) ->
new MyPublicRetryException("Retries exhausted", retrySignal.failure())
)
)This approach guarantees that your code can handle the exhausted scenario with an exception type you control and expose publicly[1][3].
Summary Table: Retry Exhaustion Exception Handling
| Library/Module | Exception Type | Visibility | Can You Catch/Public? |
|---|---|---|---|
| reactor-core | Internal retry exhausted | package-private | No—unless you override |
| reactor-extra | RetryExhaustedException | public | Yes |
| Custom (recommended) | YourOwnException | public | Yes |
Key Point:
If you depend on exception type in application code, always customize via onRetryExhaustedThrow to avoid issues with package-private visibility and ensure your code remains maintainable and robust[1][3][7].
Citations:
- 1: https://www.baeldung.com/spring-webflux-retry
- 2: https://www.baeldung.com/java-access-modifiers
- 3: https://dev.to/tharindufdo/spring-webflux-retry-mechanism-8f4
- 4: https://trifork.nl/blog/retry-functionality-in-a-reactive-programming-context/
- 5: https://discuss.kotlinlang.org/t/another-call-for-package-private-visibility/9577
- 6: retryWhen working behavior doubt reactor/reactor-core#2438
- 7: Retry.retryBackOff() should throw a meaningful exception on retries exhausted reactor/reactor-core#2052
- 8: https://dev.to/saladlam/project-reactor-about-fuseable-interface-sync-mode-4gah
- 9: https://projectreactor.io/docs/core/release/api/reactor/util/retry/Retry.html
- 10: https://projectreactor.io/docs/extra/3.3.0.M1/api/reactor/retry/RetryExhaustedException.html
@thezz9 맞습니다! 확인 결과 reactor-core에서 재시도가 모두 실패했을 때 던지는 예외는 실제로 package-private이어서 직접 import하거나 catch할 수 없네요.
현재 접근 방식이 합리적인 해결책입니다. 다만 두 가지 개선 방안을 제안드립니다:
방안 1: 현재 방식 + 주석 추가 (간단함)
// Reactor's retry exhausted exception is package-private, catch parent IllegalStateException
.onErrorMap(IllegalStateException.class,
ex -> new SubmissionException(SubmissionExceptionCode.COMPILE_TIMEOUT))방안 2: onRetryExhaustedThrow 사용 (더 명확함)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
.maxBackoff(Duration.ofSeconds(4))
.filter(ex -> ex instanceof WebClientResponseException || ex instanceof TimeoutException)
.onRetryExhaustedThrow((retrySpec, retrySignal) ->
new SubmissionException(SubmissionExceptionCode.COMPILE_TIMEOUT))
)현재 방식도 충분히 좋으니 주석만 추가하셔도 됩니다!
✏️ Learnings added
Learnt from: thezz9
PR: ezcode-my/backend#120
File: src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0Client.java:58-64
Timestamp: 2025-06-30T10:10:07.420Z
Learning: Judge0Client에서 Reactor의 retryWhen() 재시도가 모두 실패했을 때 발생하는 RetryExhaustedException은 package-private 클래스라서 직접 import할 수 없으므로, 부모 클래스인 IllegalStateException을 catch해서 COMPILE_TIMEOUT으로 매핑하는 것이 필요한 해결책이다.
Learnt from: thezz9
PR: ezcode-my/backend#120
File: src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0Client.java:58-64
Timestamp: 2025-06-30T10:10:07.421Z
Learning: Reactor의 retryWhen()에서 재시도가 모두 실패했을 때 발생하는 retry exhausted exception은 reactor-core에서 package-private으로 구현되어 있어서 직접 import하거나 catch할 수 없다. 따라서 부모 클래스를 catch하거나 onRetryExhaustedThrow()를 사용해서 커스텀 예외를 던지는 방식으로 처리해야 한다.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 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#98
File: src/main/java/org/ezcode/codetest/application/submission/dto/event/SubmissionErrorEvent.java:17-22
Timestamp: 2025-06-25T01:24:30.754Z
Learning: JudgeClient에서 모든 예외를 SubmissionException으로 래핑해서 넘길 계획이므로, SubmissionErrorEvent.resolveCode() 메서드의 캐스팅은 안전할 예정임.
Learnt from: thezz9
PR: ezcode-my/backend#73
File: src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIReviewClient.java:52-64
Timestamp: 2025-06-18T11:42:03.701Z
Learning: OpenAI API 클라이언트에서는 두 가지 재시도 전략을 사용한다: 1) WebClient의 retryWhen은 네트워크/HTTP 에러 및 타임아웃에 대한 재시도, 2) 외부 for 루프는 API 응답 포맷 검증 실패에 대한 재시도. 이는 서로 다른 실패 상황을 처리하는 독립적인 메커니즘이다.
Learnt from: chat26666
PR: ezcode-my/backend#88
File: src/main/java/org/ezcode/codetest/application/game/play/GamePlayUseCase.java:0-0
Timestamp: 2025-06-21T21:29:21.785Z
Learning: JwtUtil.extractClaims() 메서드는 이미 JwtException을 catch하여 ServerException으로 변환하는 예외 처리가 구현되어 있으므로, 호출하는 곳에서 추가적인 try-catch 블록이 필요하지 않습니다.
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#114
File: src/main/java/org/ezcode/codetest/infrastructure/github/GitHubApiClient.java:93-96
Timestamp: 2025-06-28T02:04:24.910Z
Learning: User thezz9 sometimes temporarily uses less secure or inconsistent exception handling (like RuntimeException with raw error bodies) for debugging purposes when error messages are being hidden in the normal application flow, but acknowledges this as technical debt and fixes it once debugging is complete.
Learnt from: thezz9
PR: ezcode-my/backend#114
File: src/main/java/org/ezcode/codetest/infrastructure/github/GitHubApiClient.java:159-165
Timestamp: 2025-06-28T02:03:38.654Z
Learning: User thezz9's service (ezcode) targets Korean users exclusively ("찐 한국인밖에 안 옴"), so Korean language hardcoding in commit messages and UI elements is intentional and appropriate for their use case.
작업 내용
Summary by CodeRabbit
버그 수정
테스트
작업