# 콜드메일 `skill-rag-file` 500과 실패가시성 복구

tags: [coldmail, skill-rag-file, rb8001, postgres, scheduler, troubleshooting]

**날짜**: 2026-03-10  
**작성자**: Codex  
**관련 파일**: `rb8001/app/services/coldmail_processor.py`, `rb8001/app/services/workflows/coldmail_workflow.py`, `rb8001/app/scheduler/jobs/coldmail_briefing.py`, `rb8001/tests/test_coldmail_failure_visibility.py`, `skill-rag-file/app/api/upload.py`, `skill-rag-file/.env`  
**상위 원칙**: [전역 원칙](../../../../0_VALUE/00_Foundations/헌장.md), [문서 작성 원칙](../../book/300_architecture/312_writing-principles.md), [Backend Coding Principles](../../book/300_architecture/311_backend_coding_principles.md)

## 관련 문서
- [콜드메일 `skill-rag-file` 500 및 실패은닉 원인확정 리서치](../research/260310_콜드메일_skill_rag_file_500_및_실패은닉_원인확정_리서치.md)
- [9시 네이버 이메일 분석 미전송과 실패 은닉 해결](./260309_9시_네이버이메일분석_미전송_실패은닉_해결.md)
- [51123 임시복구 서비스 연속성 조치내역](../../../../infra/DOCS/journey/troubleshooting/260304_51123_임시복구_서비스연속성_조치내역.md)

---

## 문제 상황
- `2026-03-10 09:05` KST 콜드메일 작업은 실제 실행됐지만, 첨부 PDF 업로드가 전부 실패해 결과가 0건으로 끝났다.
- `skill-rag-file`은 `POST /api/upload`에서 `500 Internal Server Error`를 반환했고, 실제 스택트레이스는 DB timeout이었다.
- `rb8001` 상위 흐름은 이 실패를 예외로 올리지 않아 APScheduler에는 성공처럼 보였다.

## 근본 원인
- `skill-rag-file/.env`가 과거 임시복구용 DB 주소 `192.168.0.100:5432`를 계속 우선 사용하고 있었다.
- 현재 서버에서 해당 주소는 더 이상 유효하지 않아 DB insert 시 timeout이 발생했다.
- `rb8001`은 첨부 처리 실패를 `None`으로 흡수해 "0건 처리"로만 닫는 구조였다.

## 조치
- `skill-rag-file/.env`의 `DATABASE_URL` 호스트를 `172.17.0.1`로 교정했다.
- `skill-rag-file/app/api/upload.py`에서 DB 비가용성 예외만 `503`으로 분리하고, 없는 `team_id`는 `404`로 반환하도록 정리했다.
- `rb8001/app/services/coldmail_processor.py`에서 첨부 처리 실패를 `ColdmailProcessingError`로 승격했다.
- `rb8001/app/services/workflows/coldmail_workflow.py`에서 실패 목록을 `processing_failures`로 수집하게 바꿨다.
- `rb8001/app/scheduler/jobs/coldmail_briefing.py`에서 실패 목록이 있으면 예외를 다시 올리도록 바꿨다.
- `rb8001/tests/test_coldmail_failure_visibility.py`를 추가해 실패 수집과 스케줄러 예외 승격을 검증했다.

## 검증
- `python3 -m py_compile`로 `rb8001` 변경 파일, `skill-rag-file/app/api/upload.py`, 테스트 파일 컴파일을 확인했다.
- `docker compose ... up -d --build`로 `rb8001`, `skill-rag-file` 재배포를 완료했다.
- 실제 팀 ID `38bdc27d-cb01-4960-867e-41733d2f3529` 업로드 응답은 `200`이었다.
- 없는 팀 ID `00000000-0000-0000-0000-000000000001` 업로드 응답은 `404`였다.
- 재검증 시 `skill-rag-file` 로그에는 더 이상 DB timeout 기반 `500`이 남지 않았다.

## 구현 결과
- 콜드메일 첨부 업로드 실패는 더 이상 스케줄러 성공으로 숨겨지지 않는다.
- `skill-rag-file`의 상태코드가 원인성에 맞게 정리됐다.
- 오늘 09:05 장애의 직접 원인인 stale DB host 경로는 제거됐다.

## 교훈
- 서비스별 `.env` 오버라이드는 SSOT 런타임 값을 덮기 때문에, 임시복구 값이 남아 있으면 후속 장애의 직접 원인이 된다.
- 외부/하위 서비스 실패를 `None`이나 `0건`으로 흡수하면 운영 판단이 틀어진다.
- 비가용성, 입력 오류, 무결성 오류는 서로 다른 상태코드로 유지해야 원인 추적이 가능하다.
