-
Notifications
You must be signed in to change notification settings - Fork 1
AI 이미지 생성 응답 큐 DLQ 및 재시도 메커니즘 추가 #213
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
Changes from all commits
7a0a619
62d5146
a1e7a64
34bda8e
bbe9d16
a41a3b9
c199bed
89fac9d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| package hanium.modic.backend.domain.ai.aiServer.listener; | ||
|
|
||
| import static hanium.modic.backend.common.amqp.config.RabbitMqConfig.*; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| import org.springframework.amqp.core.Message; | ||
| import org.springframework.amqp.rabbit.annotation.RabbitListener; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import hanium.modic.backend.domain.ai.aiChat.entity.AiChatMessageEntity; | ||
| import hanium.modic.backend.domain.ai.aiChat.repository.AiChatMessageRepository; | ||
| import hanium.modic.backend.domain.ai.aiServer.dto.AiImageResponseMessageDto; | ||
| import hanium.modic.backend.domain.ai.aiServer.enums.AiImageStatus; | ||
| import hanium.modic.backend.domain.ai.aiServer.enums.SenderType; | ||
| import hanium.modic.backend.domain.ai.aiServer.service.AiResponseSseService; | ||
| import hanium.modic.backend.web.ai.aiChat.dto.response.ChatMessageResponse; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| @Slf4j | ||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class AiImageCreatedDlqListener { | ||
|
|
||
| private final AiChatMessageRepository aiChatMessageRepository; | ||
| private final AiResponseSseService aiResponseSseService; | ||
|
|
||
| /** | ||
| * AI 이미지 생성 응답 처리 최종 실패 메시지 처리 | ||
| * - 메시지 상태를 RESPONSE_FAILED로 변경 | ||
| * - SSE 연결이 있으면 클라이언트에 실패 알림 | ||
| * @param messageDto AI 이미지 응답 메시지 | ||
| * @param message RabbitMQ 메시지 메타데이터 | ||
| */ | ||
| @Transactional | ||
| @RabbitListener(queues = AI_IMAGE_CREATED_DLQ) | ||
| public void handleFinalFailedMessage(AiImageResponseMessageDto messageDto, Message message) { | ||
| log.error("[최종 실패] AI 이미지 생성 응답 처리 최종 실패: requestId={}", messageDto.requestId()); | ||
|
|
||
| // 1. 요청 메시지 조회 | ||
| Optional<AiChatMessageEntity> chatMessageOpt = aiChatMessageRepository | ||
| .findByRequestIdAndSenderType(messageDto.requestId(), SenderType.USER); | ||
|
|
||
| if (chatMessageOpt.isEmpty()) { | ||
| log.error("[데이터 오류] requestId={}에 해당하는 채팅 메시지를 찾을 수 없습니다", messageDto.requestId()); | ||
| return; | ||
| } | ||
|
|
||
| AiChatMessageEntity requestChatMessage = chatMessageOpt.get(); | ||
|
|
||
| // 2. 메시지 상태를 RESPONSE_FAILED로 변경 | ||
| requestChatMessage.updateStatus(AiImageStatus.RESPONSE_FAILED); | ||
| aiChatMessageRepository.save(requestChatMessage); | ||
| log.info("[상태 업데이트] requestId={} 메시지 상태를 RESPONSE_FAILED로 변경", messageDto.requestId()); | ||
|
|
||
| // 3. SSE 연결이 있으면 클라이언트에 실패 알림 | ||
| try { | ||
| ChatMessageResponse errorResponse = ChatMessageResponse.createErrorResponse( | ||
| requestChatMessage, | ||
| "이미지 생성 응답 처리에 실패했습니다. 잠시 후 다시 시도해주세요." | ||
| ); | ||
| aiResponseSseService.sendToClient(messageDto.requestId(), errorResponse); | ||
| log.info("[SSE 알림] requestId={} 클라이언트에 실패 알림 전송", messageDto.requestId()); | ||
| } catch (Exception e) { | ||
| log.warn("[SSE 알림 실패] requestId={} SSE 연결이 없거나 전송 실패: {}", | ||
| messageDto.requestId(), e.getMessage()); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -35,4 +35,17 @@ public static ChatMessageResponse from(AiChatMessageEntity entity, String imageU | |||||||||||||||||||||||||||||||||||||||||||||||
| public static ChatMessageResponse from(AiChatMessageEntity entity) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return from(entity, null); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| public static ChatMessageResponse createErrorResponse(AiChatMessageEntity entity, String errorMessage) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return new ChatMessageResponse( | ||||||||||||||||||||||||||||||||||||||||||||||||
| entity.getId(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| entity.getMessageOrder(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| SenderType.AI, | ||||||||||||||||||||||||||||||||||||||||||||||||
| errorMessage, | ||||||||||||||||||||||||||||||||||||||||||||||||
| entity.getRequestId(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| null, | ||||||||||||||||||||||||||||||||||||||||||||||||
| entity.getCreateAt(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| AiImageStatus.RESPONSE_FAILED | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+39
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오류 응답의 senderType 하드코딩으로 인한 불일치 USER 요청 엔티티의 id/order를 그대로 쓰면서 senderType을 AI로 고정하면, 클라이언트가 senderType 기반 렌더링/정렬 시 혼선이 생깁니다. 상태 변경 알림이라면 엔티티의 senderType을 그대로 사용하세요. 또한 createdAt을 요청 시각 대신 now로 표기하는 것도 고려해 주세요(선택). 필수 수정(불일치 해소): - SenderType.AI,
+ entity.getSenderType(),선택(타임스탬프 의미 명확화): - entity.getCreateAt(),
+ LocalDateTime.now(),📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
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
현재 구성은 무한 재시도 루프가 됩니다 — DLQ로 절대 도달하지 않음
두 가지 해결책 중 하나를 권장합니다:
@@ public Queue aiImageCreatedRetryQueue() { - args.put("x-dead-letter-exchange", AI_IMAGE_CREATED_EXCHANGE); - args.put("x-dead-letter-routing-key", AI_IMAGE_CREATED_ROUTING_KEY); + // TTL 만료 시 최종 DLQ로 보냄 (지연 재시도는 1회로 제한) + args.put("x-dead-letter-exchange", AI_IMAGE_CREATED_DLX); + args.put("x-dead-letter-routing-key", AI_IMAGE_CREATED_DLQ_ROUTING_KEY); }이렇게 하면: 원래 큐에서 1회 처리 실패 → 재시도 큐로 이동(60초 대기) → DLQ로 최종 이동(무한 루프 차단).
예:
참고: Request 경로도 동일한 패턴(무한 루프)입니다. 동일한 방식으로 정리하시길 권장합니다.
무한 루프가 의도인지, 최종 DLQ로 보내고자 하는 최대 재시도 횟수(또는 지연 전략)를 알려주시면 정확한 설정/코드 패치를 드리겠습니다.
Also applies to: 128-136, 147-154, 102-120
다음 스크립트를 실행하여
RabbitMqConfig.java의 DLX/TTL 및 컨테이너 팩토리 재시도 설정을 확인하겠습니다.🏁 Script executed:
Length of output: 1496
무한 재시도 루ープ 방지 및 DLQ 도달 설정 필요
현재 aiImageCreatedQueue → 재시도큐 → 원큐 반복만 수행되어 최종 DLQ(“…dlq”)에는 메시지가 도달하지 않음
아래 두 가지 중 하나로 수정하세요
@@ public Queue aiImageCreatedRetryQueue() { - args.put("x-dead-letter-exchange", AI_IMAGE_CREATED_EXCHANGE); - args.put("x-dead-letter-routing-key", AI_IMAGE_CREATED_ROUTING_KEY); + // 1회 지연 후 최종 DLQ로 이동 + args.put("x-dead-letter-exchange", AI_IMAGE_CREATED_DLX); + args.put("x-dead-letter-routing-key", AI_IMAGE_CREATED_DLQ_ROUTING_KEY); }rabbitListenerContainerFactory에RetryInterceptorBuilder.stateless().maxAttempts(3)…recoverer(new RepublishMessageRecoverer(rabbitTemplate, AI_IMAGE_CREATED_DLX, AI_IMAGE_CREATED_DLQ_ROUTING_KEY))추가하여 N회 실패 시 DLQ로 발행동일 패턴이 Request 큐에도 적용되어 있으니 함께 수정 필요
🤖 Prompt for AI Agents