-
Notifications
You must be signed in to change notification settings - Fork 0
[EDMT-459] AI 실패 시 포인트 보상을 위한 Saga 패턴 구현 #80
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
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
a89ed05
[feat] add FAILED status to AITaskStatus and update progress check
kgy1008 869fc60
[feat] add AIErrorMessage and AITaskFailedEvent records for error han…
kgy1008 85395fc
[feat] enhance error handling by adding AIErrorType enum and updating…
kgy1008 f58ddf4
[feat] add failure tracking columns to student_record_ai_task table
kgy1008 8d0645a
[feat] implement error message handling in SSEChannelManager and SSEM…
kgy1008 62f18bd
[feat] handle AI task failure by processing error messages and publis…
kgy1008 fbdef40
[feat] implement AI task failure handling and compensation logic
kgy1008 eb90185
[refac] remove unused fromErrorMessage method in AITaskFailedEvent
kgy1008 16b572e
[fix] add AITaskFailedEvent class and update event handling in AIComp…
kgy1008 476bdf0
[feat] add fromString method to AIErrorType for error type retrieval
kgy1008 a1396a6
[refac] refactor point deduction and compensation methods in Member a…
kgy1008 801911a
[refac] refactor AICompensationListener to use EventListener for AITa…
kgy1008 3aaacaa
[refac] enhance compensation handling in AICompensationListener with …
kgy1008 6c828a2
[refac] enhance fromString method in AIErrorType to handle null and b…
kgy1008 72c9f2b
[refac] improve error handling in handleAITaskFailure method for bett…
kgy1008 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
5 changes: 5 additions & 0 deletions
5
edukit-api/src/main/resources/db/migration/V12__Add_ai_task_failure_tracking.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| -- Add failure tracking columns to student_record_ai_task table | ||
|
|
||
| ALTER TABLE student_record_ai_task | ||
| ADD COLUMN failed_at DATETIME NULL AFTER completed_at, | ||
| ADD COLUMN error_type VARCHAR(50) NULL AFTER failed_at; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
75 changes: 75 additions & 0 deletions
75
edukit-core/src/main/java/com/edukit/core/event/ai/AICompensationListener.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| package com.edukit.core.event.ai; | ||
|
|
||
| import com.edukit.core.common.service.RedisStoreService; | ||
| import com.edukit.core.event.ai.dto.AITaskFailedEvent; | ||
| import com.edukit.core.point.service.PointService; | ||
| import com.edukit.core.studentrecord.db.entity.StudentRecordAITask; | ||
| import com.edukit.core.studentrecord.service.AITaskService; | ||
| import java.time.Duration; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; | ||
| import org.springframework.context.event.EventListener; | ||
| import org.springframework.scheduling.annotation.Async; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| @Slf4j | ||
| @Component | ||
| @RequiredArgsConstructor | ||
| @ConditionalOnBean(RedisStoreService.class) | ||
| public class AICompensationListener { | ||
|
|
||
| private final PointService pointService; | ||
| private final AITaskService aiTaskService; | ||
| private final RedisStoreService redisStoreService; | ||
|
|
||
| private static final String COMPENSATION_KEY_PREFIX = "compensation:"; | ||
| private static final int DEDUCTED_POINTS = 100; | ||
| private static final Duration COMPENSATION_RECORD_TTL = Duration.ofDays(7); | ||
|
|
||
| @Async("aiTaskExecutor") | ||
| @EventListener | ||
| public void handleAITaskFailure(final AITaskFailedEvent event) { | ||
| String taskId = event.taskId(); | ||
| String compensationKey = COMPENSATION_KEY_PREFIX + taskId; | ||
|
|
||
| // 원자적 선점 - Redis SET NX로 중복 보상 방지 | ||
| if (!tryClaimCompensation(compensationKey)) { | ||
| log.warn("Task {} already compensated by another instance, skipping", taskId); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| // Task 정보 조회 | ||
| StudentRecordAITask task = aiTaskService.getTaskById(Long.valueOf(taskId)); | ||
|
|
||
| // Task를 실패로 마킹 | ||
| aiTaskService.markTaskAsFailed(Long.valueOf(taskId), event.errorType()); | ||
|
|
||
| // 포인트 보상 | ||
| pointService.compensatePoints( | ||
| task.getMember().getId(), | ||
| DEDUCTED_POINTS, | ||
| task.getId() | ||
| ); | ||
|
|
||
| log.info("Successfully compensated {} points for taskId: {} (errorType: {})", | ||
| DEDUCTED_POINTS, taskId, event.errorType()); | ||
|
|
||
| } catch (Exception e) { | ||
| log.error("Failed to compensate points for taskId: {}", taskId, e); | ||
| // 보상 실패 시 Redis 키 삭제하여 재시도 가능하게 함 | ||
| redisStoreService.delete(compensationKey); | ||
| throw e; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * 원자적 선점 시도 | ||
| * @return true: 선점 성공 (보상 실행), false: 이미 다른 인스턴스가 선점 (스킵) | ||
| */ | ||
| private boolean tryClaimCompensation(final String compensationKey) { | ||
| Boolean claimed = redisStoreService.setIfAbsent(compensationKey, "COMPENSATED", COMPENSATION_RECORD_TTL); | ||
| return Boolean.TRUE.equals(claimed); | ||
| } | ||
| } |
17 changes: 17 additions & 0 deletions
17
edukit-core/src/main/java/com/edukit/core/event/ai/dto/AIErrorMessage.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.edukit.core.event.ai.dto; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
|
|
||
| public record AIErrorMessage( | ||
| @JsonProperty("task_id") | ||
| String taskId, | ||
| @JsonProperty("status") | ||
| String status, | ||
| @JsonProperty("error_type") | ||
| String errorType, | ||
| @JsonProperty("error_message") | ||
| String errorMessage, | ||
| @JsonProperty("retryable") | ||
| Boolean retryable | ||
| ) { | ||
| } |
18 changes: 18 additions & 0 deletions
18
edukit-core/src/main/java/com/edukit/core/event/ai/dto/AITaskFailedEvent.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.edukit.core.event.ai.dto; | ||
|
|
||
| public record AITaskFailedEvent( | ||
| String taskId, | ||
| String errorType | ||
| ) { | ||
|
|
||
| public static AITaskFailedEvent of(final String taskId, final String errorType) { | ||
| return new AITaskFailedEvent(taskId, errorType); | ||
| } | ||
|
|
||
| public static AITaskFailedEvent fromErrorMessage(final AIErrorMessage errorMessage) { | ||
| return AITaskFailedEvent.of( | ||
| errorMessage.taskId(), | ||
| errorMessage.errorType() | ||
| ); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
26 changes: 26 additions & 0 deletions
26
edukit-core/src/main/java/com/edukit/core/studentrecord/db/enums/AIErrorType.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package com.edukit.core.studentrecord.db.enums; | ||
|
|
||
| import java.util.Arrays; | ||
| import lombok.Getter; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Getter | ||
| @RequiredArgsConstructor | ||
| public enum AIErrorType { | ||
| OPENAI_API_ERROR("OpenAI API 호출 실패"), | ||
| LAMBDA_ERROR("Lambda 처리 오류"), | ||
| UNKNOWN_ERROR("알 수 없는 오류"); | ||
|
|
||
| private final String description; | ||
|
|
||
| public static AIErrorType fromString(final String value) { | ||
| if (value == null || value.isBlank()) { | ||
| return UNKNOWN_ERROR; | ||
| } | ||
|
|
||
| return Arrays.stream(AIErrorType.values()) | ||
| .filter(type -> type.name().equalsIgnoreCase(value)) | ||
| .findFirst() | ||
| .orElse(UNKNOWN_ERROR); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.