feat(ai,core): 질문/꼬리질문 생성 실패 시 무기한 대기 제거 + 타임아웃 하드닝 - #144
Merged
Conversation
questions_consumer/followup_consumer 의 메인 생성 호출(generate()/stream())이 무방비라 실패하면 예외가 그대로 새서 DLQ 로 격리되고 Core 는 아무 신호도 못 받아 세션이 '생성 중'에 무기한 멈추는 문제(꼬리질문은 Core 가 이미 선INSERT 한 placeholder 가 영원히 안 채워짐). 두 consumer 모두 생성 호출을 try/except 로 감싸 실패해도 항상 콜백(status=FAILED, errorCode, errorMessage, retriable)을 발행하도록 수정. TypeError(LLM 출력 스키마 불일치)는 재시도 무의미하다고 보고 retriable=false, 그 외는 retriable=true 로 구분. 추가로: - questions_consumer 의 다문서 RAG 경로에 questions_rag_timeout_sec(1.5s) 타임아웃 추가 — followup_consumer 와 대칭 - 모든 ChatOpenAI 호출에 llm_pro_timeout_sec(30s)/llm_flash_timeout_sec(10s) 요청 타임아웃 명시 — 미설정 시 SDK 기본값까지 무기한 대기 가능했음 - docs/messaging.md, docs/environment.md, .env.example 갱신
QuestionsCallbackPayload(Java)에 status/errorCode/errorMessage/retriable 필드 추가(구버전 9-arg 생성자는 status=OK 로 위임하는 오버로드로 하위호환 유지). QuestionsCallbackService.apply 가 kind 분기 전에 isFailed() 를 먼저 확인: - POOL 실패: 저장할 게 없으니 SseEventType.ERROR(SessionErrorNotice)로 세션/유저 채널에만 알린다. 세션 상태는 그대로 유지(재시도 트리거는 후속 과제). - FOLLOWUP 실패: placeholder 를 삭제하지 않고 InterviewMessage.failFollowup() (content=FOLLOWUP_GENERATION_FAILED_TEXT, status=FAILED)로 확정한 뒤 SESSION_MESSAGE(FOLLOWUP_FAILED) 발행 + DONT_KNOW 와 동일하게 advanceToNextGeneral 로 다음 일반질문으로 진행 — 턴이 사라진 것처럼 보이지 않으면서 면접은 멈추지 않는다. 회귀 테스트 2건 추가(POOL 실패 시 에러 이벤트, FOLLOWUP 실패 시 placeholder 확정+진행). backend/CLAUDE.md 갱신.
2 tasks
This was referenced Aug 20, 2026
i3months
added a commit
to i3months/stackup
that referenced
this pull request
Aug 28, 2026
dev 배포가 PR Team-StackUp#144 부터 4회 연속 실패했다. 원인은 백엔드 Docker 빌드: Exception in thread "main" java.io.IOException: Downloading from https://services.gradle.org/distributions/gradle-9.4.0-bin.zip failed: timeout (10000ms) 베이스 이미지가 gradle:8.12-jdk21 인데 wrapper 는 9.4.0 을 요구해서, 매 빌드마다 배포판(약 130MB)을 새로 내려받고 있었다. self-hosted runner 에서 services.gradle.org 접속이 타임아웃돼(wrapper 기본 10s) 실패한다. 재시도해도 27초만에 같은 지점에서 죽어 일시적 문제가 아니었다. Team-StackUp#143 까지 성공한 건 그 PR 들이 backend/ 를 건드리지 않아 Docker 레이어 캐시가 살아있었기 때문이다. Team-StackUp#144 에서 backend 소스가 바뀌자 캐시가 깨지고 증상이 드러났다. 수정: 베이스 이미지를 gradle:9.4.0-jdk21 로 올리고 wrapper 대신 이미지에 들어있는 gradle 을 쓴다 → 네트워크 의존이 사라진다. 태그는 gradle-wrapper.properties 의 버전과 정확히 맞췄다(9.4-jdk21 은 9.4.1 로 해석돼 로컬과 어긋난다). 의존성 해결을 별도 레이어로 분리해 소스만 바뀔 때 재다운로드도 건너뛴다. wrapper 의 networkTimeout 은 10s → 120s. Docker 빌드와는 무관해졌지만 로컬에서 ./gradlew 로 빌드할 때의 여유를 둔다. 검증: 로컬에서 docker build 성공(BUILD SUCCESSFUL, 배포판 다운로드 없음), ./gradlew --version 도 9.4.0 으로 이미지와 일치.
i3months
added a commit
to i3months/stackup
that referenced
this pull request
Aug 28, 2026
음성 답변은 `(transcribing)` placeholder 로 먼저 저장되고 `callback.voice` 가 도착해야 내용이 채워진다. 그 콜백이 오지 않으면(AI 크래시·DLQ 격리·브로커 단절) 메시지는 그 상태로 남는다. 프론트 턴 판정(`currentTurn`)에서 이 메시지는 FAILED 도 아니고 질문도 아니라 **답변 차례가 영영 오지 않는다** — 세션 시간 초과로 면접이 통째로 끝날 때까지 사용자는 "답변을 받아 적고 있어요…" 화면에 갇힌다. 질문 생성 쪽은 Team-StackUp#144 로 실패 신호(callback status=FAILED)를 만들어 같은 문제를 없앴는데 음성 경로에는 대응이 없었다. - `StaleTranscriptionSweeper` — 기본 2분 주기로 `interview.voice.stale-transcription-minutes` (기본 5분)를 넘긴 placeholder 를 찾는다. 진행 중·미삭제 세션의 것만 본다 - `VoiceTranscriptionRecoveryService.failStaleTranscription` — FAILED 로 확정하고 세션·유저 채널에 SSE 통지(`STT_CALLBACK_TIMEOUT`). 메시지마다 독립 트랜잭션이라 하나가 실패해도 나머지는 정리된다 - 확정 직전 상태를 다시 확인한다 — 스위퍼가 목록을 만든 뒤 콜백이 도착했으면 건드리지 않는다(완료된 답변을 실패로 되돌리는 것이 더 나쁘다) FAILED 로 두면 기존 STT 실패 경로를 그대로 탄다: 프론트 `currentTurn` 의 INTERVIEWEE+FAILED 분기가 답변 차례로 돌리고, 백엔드 `resolveAnswerParent` 가 같은 질문에 대한 텍스트 재답변을 받는다. 새 UX 를 만들지 않고 이미 검증된 경로에 합류시켰다. 테스트 `VoiceTranscriptionRecoveryServiceTest` (3) — FAILED 확정 + 양 채널 통지, 그 사이 전사가 도착한 경우 건드리지 않음, 메시지 부재 시 무동작.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
questions_consumer/followup_consumer의 메인 생성 호출(generate()/stream())이 무방비라 실패하면 예외가 그대로 새서 DLQ로 격리되고 Core는 아무 신호도 못 받아 세션이 "생성 중"에 무기한 멈추는 문제를 고침. 특히 꼬리질문은 Core가 이미 선INSERT한 placeholder가 영원히 안 채워지는 더 나쁜 증상이 있었음.status=FAILED(+errorCode/errorMessage/retriable) 콜백을 발행하도록 수정.QuestionsCallbackPayload에 같은 필드 추가(구버전 9-arg 생성자는 하위호환 오버로드로 유지). POOL 실패는 SSEERROR이벤트로만 알리고, FOLLOWUP 실패는 placeholder를failFollowup()으로 확정 표시 후 DONT_KNOW와 동일하게 다음 질문으로 진행.questions_consumer의 다문서 RAG 경로에 타임아웃 추가(followup과 대칭), 모든ChatOpenAI호출에 요청 타임아웃 명시(이전엔 미설정 — SDK 기본값까지 무기한 대기 가능).호환성
AI/Core 어느 쪽이 먼저 배포돼도 안전함 — 구버전 Core는 새 필드를 무시(Jackson 기본 동작)하고, 구버전 AI는 애초에 새 필드를 안 보냄.
스코프 밖(후속 과제로 남김)
Test plan
uv run pytest321개 전체 통과 (신규 6건)./gradlew test126개 전체 통과 (신규 2건)