merge main
parent
e30e7304df
commit
dc7351d0f9
|
|
@ -0,0 +1,571 @@
|
|||
# Suno API & Creatomate API 에러 처리 작업 계획서
|
||||
|
||||
## 현재 상태 분석
|
||||
|
||||
### ✅ 이미 구현된 항목
|
||||
| 항목 | Suno API | Creatomate API |
|
||||
|------|----------|----------------|
|
||||
| DB failed 상태 저장 | ✅ `song_task.py` | ✅ `video_task.py` |
|
||||
| Response failed 상태 반환 | ✅ `song_schema.py` | ✅ `video_schema.py` |
|
||||
|
||||
### ❌ 미구현 항목
|
||||
| 항목 | Suno API | Creatomate API |
|
||||
|------|----------|----------------|
|
||||
| 타임아웃 외부화 | ❌ 하드코딩됨 | ❌ 하드코딩됨 |
|
||||
| 재시도 로직 | ❌ 없음 | ❌ 없음 |
|
||||
| 커스텀 예외 클래스 | ❌ 없음 | ❌ 없음 |
|
||||
|
||||
---
|
||||
|
||||
## 1. RecoverySettings에 Suno/Creatomate 설정 추가
|
||||
|
||||
### 파일: `config.py`
|
||||
|
||||
**변경 전:**
|
||||
```python
|
||||
class RecoverySettings(BaseSettings):
|
||||
"""ChatGPT API 복구 및 타임아웃 설정"""
|
||||
|
||||
CHATGPT_TIMEOUT: float = Field(
|
||||
default=600.0,
|
||||
description="ChatGPT API 타임아웃 (초). OpenAI Python SDK 기본값: 600초 (10분)",
|
||||
)
|
||||
CHATGPT_MAX_RETRIES: int = Field(
|
||||
default=1,
|
||||
description="ChatGPT API 응답 실패 시 최대 재시도 횟수",
|
||||
)
|
||||
|
||||
model_config = _base_config
|
||||
```
|
||||
|
||||
**변경 후:**
|
||||
```python
|
||||
class RecoverySettings(BaseSettings):
|
||||
"""외부 API 복구 및 타임아웃 설정
|
||||
|
||||
ChatGPT, Suno, Creatomate API의 타임아웃 및 재시도 설정을 관리합니다.
|
||||
"""
|
||||
|
||||
# ============================================================
|
||||
# ChatGPT API 설정
|
||||
# ============================================================
|
||||
CHATGPT_TIMEOUT: float = Field(
|
||||
default=600.0,
|
||||
description="ChatGPT API 타임아웃 (초). OpenAI Python SDK 기본값: 600초 (10분)",
|
||||
)
|
||||
CHATGPT_MAX_RETRIES: int = Field(
|
||||
default=1,
|
||||
description="ChatGPT API 응답 실패 시 최대 재시도 횟수",
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# Suno API 설정
|
||||
# ============================================================
|
||||
SUNO_DEFAULT_TIMEOUT: float = Field(
|
||||
default=30.0,
|
||||
description="Suno API 기본 요청 타임아웃 (초)",
|
||||
)
|
||||
SUNO_LYRIC_TIMEOUT: float = Field(
|
||||
default=120.0,
|
||||
description="Suno API 가사 타임스탬프 요청 타임아웃 (초)",
|
||||
)
|
||||
SUNO_MAX_RETRIES: int = Field(
|
||||
default=2,
|
||||
description="Suno API 응답 실패 시 최대 재시도 횟수",
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# Creatomate API 설정
|
||||
# ============================================================
|
||||
CREATOMATE_DEFAULT_TIMEOUT: float = Field(
|
||||
default=30.0,
|
||||
description="Creatomate API 기본 요청 타임아웃 (초)",
|
||||
)
|
||||
CREATOMATE_RENDER_TIMEOUT: float = Field(
|
||||
default=60.0,
|
||||
description="Creatomate API 렌더링 요청 타임아웃 (초)",
|
||||
)
|
||||
CREATOMATE_CONNECT_TIMEOUT: float = Field(
|
||||
default=10.0,
|
||||
description="Creatomate API 연결 타임아웃 (초)",
|
||||
)
|
||||
CREATOMATE_MAX_RETRIES: int = Field(
|
||||
default=2,
|
||||
description="Creatomate API 응답 실패 시 최대 재시도 횟수",
|
||||
)
|
||||
|
||||
model_config = _base_config
|
||||
```
|
||||
|
||||
**이유:** 모든 외부 API의 타임아웃/재시도 설정을 `RecoverySettings` 하나에서 통합 관리하여 일관성을 유지합니다.
|
||||
|
||||
### 파일: `.env`
|
||||
|
||||
**추가할 내용:**
|
||||
```env
|
||||
# ============================================================
|
||||
# 외부 API 타임아웃 및 재시도 설정 (RecoverySettings)
|
||||
# ============================================================
|
||||
|
||||
# ChatGPT API (기존)
|
||||
CHATGPT_TIMEOUT=600.0
|
||||
CHATGPT_MAX_RETRIES=1
|
||||
|
||||
# Suno API
|
||||
SUNO_DEFAULT_TIMEOUT=30.0
|
||||
SUNO_LYRIC_TIMEOUT=120.0
|
||||
SUNO_MAX_RETRIES=2
|
||||
|
||||
# Creatomate API
|
||||
CREATOMATE_DEFAULT_TIMEOUT=30.0
|
||||
CREATOMATE_RENDER_TIMEOUT=60.0
|
||||
CREATOMATE_CONNECT_TIMEOUT=10.0
|
||||
CREATOMATE_MAX_RETRIES=2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Suno API 커스텀 예외 클래스 추가
|
||||
|
||||
### 파일: `app/utils/suno.py`
|
||||
|
||||
**변경 전 (라인 1-20):**
|
||||
```python
|
||||
import httpx
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
logger = get_logger("suno")
|
||||
```
|
||||
|
||||
**변경 후:**
|
||||
```python
|
||||
import httpx
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from app.utils.logger import get_logger
|
||||
from config import recovery_settings
|
||||
|
||||
logger = get_logger("suno")
|
||||
|
||||
|
||||
class SunoResponseError(Exception):
|
||||
"""Suno API 응답 오류 시 발생하는 예외
|
||||
|
||||
Suno API 거부 응답 또는 비정상 응답 시 사용됩니다.
|
||||
재시도 로직에서 이 예외를 catch하여 재시도를 수행합니다.
|
||||
|
||||
Attributes:
|
||||
message: 에러 메시지
|
||||
original_response: 원본 API 응답 (있는 경우)
|
||||
"""
|
||||
def __init__(self, message: str, original_response: dict | None = None):
|
||||
self.message = message
|
||||
self.original_response = original_response
|
||||
super().__init__(self.message)
|
||||
```
|
||||
|
||||
**이유:** ChatGPT API와 동일하게 커스텀 예외 클래스를 추가하여 Suno API 오류를 명확히 구분하고 재시도 로직에서 활용합니다.
|
||||
|
||||
---
|
||||
|
||||
## 3. Suno API 타임아웃 적용
|
||||
|
||||
### 파일: `app/utils/suno.py`
|
||||
|
||||
**변경 전 (라인 130):**
|
||||
```python
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{self.base_url}/generate",
|
||||
headers=self.headers,
|
||||
json=payload,
|
||||
timeout=30.0,
|
||||
)
|
||||
```
|
||||
|
||||
**변경 후:**
|
||||
```python
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{self.base_url}/generate",
|
||||
headers=self.headers,
|
||||
json=payload,
|
||||
timeout=recovery_settings.SUNO_DEFAULT_TIMEOUT,
|
||||
)
|
||||
```
|
||||
|
||||
**변경 전 (라인 173):**
|
||||
```python
|
||||
timeout=30.0,
|
||||
```
|
||||
|
||||
**변경 후:**
|
||||
```python
|
||||
timeout=recovery_settings.SUNO_DEFAULT_TIMEOUT,
|
||||
```
|
||||
|
||||
**변경 전 (라인 201):**
|
||||
```python
|
||||
timeout=120.0,
|
||||
```
|
||||
|
||||
**변경 후:**
|
||||
```python
|
||||
timeout=recovery_settings.SUNO_LYRIC_TIMEOUT,
|
||||
```
|
||||
|
||||
**이유:** 환경변수로 타임아웃을 관리하여 배포 환경별로 유연하게 조정할 수 있습니다.
|
||||
|
||||
---
|
||||
|
||||
## 4. Suno API 재시도 로직 추가
|
||||
|
||||
### 파일: `app/utils/suno.py`
|
||||
|
||||
**변경 전 - generate() 메서드:**
|
||||
```python
|
||||
async def generate(
|
||||
self,
|
||||
lyric: str,
|
||||
style: str,
|
||||
title: str,
|
||||
task_id: str,
|
||||
) -> dict:
|
||||
"""음악 생성 요청"""
|
||||
payload = {
|
||||
"prompt": lyric,
|
||||
"style": style,
|
||||
"title": title,
|
||||
"customMode": True,
|
||||
"callbackUrl": f"{self.callback_url}?task_id={task_id}",
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{self.base_url}/generate",
|
||||
headers=self.headers,
|
||||
json=payload,
|
||||
timeout=30.0,
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"Failed to generate music: {response.text}")
|
||||
raise Exception(f"Failed to generate music: {response.status_code}")
|
||||
|
||||
return response.json()
|
||||
```
|
||||
|
||||
**변경 후:**
|
||||
```python
|
||||
async def generate(
|
||||
self,
|
||||
lyric: str,
|
||||
style: str,
|
||||
title: str,
|
||||
task_id: str,
|
||||
) -> dict:
|
||||
"""음악 생성 요청 (재시도 로직 포함)
|
||||
|
||||
Args:
|
||||
lyric: 가사 텍스트
|
||||
style: 음악 스타일
|
||||
title: 곡 제목
|
||||
task_id: 작업 고유 식별자
|
||||
|
||||
Returns:
|
||||
Suno API 응답 데이터
|
||||
|
||||
Raises:
|
||||
SunoResponseError: API 오류 또는 재시도 실패 시
|
||||
"""
|
||||
payload = {
|
||||
"prompt": lyric,
|
||||
"style": style,
|
||||
"title": title,
|
||||
"customMode": True,
|
||||
"callbackUrl": f"{self.callback_url}?task_id={task_id}",
|
||||
}
|
||||
|
||||
last_error: Exception | None = None
|
||||
|
||||
for attempt in range(recovery_settings.SUNO_MAX_RETRIES + 1):
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{self.base_url}/generate",
|
||||
headers=self.headers,
|
||||
json=payload,
|
||||
timeout=recovery_settings.SUNO_DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
|
||||
# 재시도 불가능한 오류 (4xx 클라이언트 오류)
|
||||
if 400 <= response.status_code < 500:
|
||||
raise SunoResponseError(
|
||||
f"Client error: {response.status_code}",
|
||||
original_response={"status": response.status_code, "text": response.text}
|
||||
)
|
||||
|
||||
# 재시도 가능한 오류 (5xx 서버 오류)
|
||||
last_error = SunoResponseError(
|
||||
f"Server error: {response.status_code}",
|
||||
original_response={"status": response.status_code, "text": response.text}
|
||||
)
|
||||
|
||||
except httpx.TimeoutException as e:
|
||||
logger.warning(f"[Suno] Timeout on attempt {attempt + 1}/{recovery_settings.SUNO_MAX_RETRIES + 1}")
|
||||
last_error = e
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.warning(f"[Suno] HTTP error on attempt {attempt + 1}: {e}")
|
||||
last_error = e
|
||||
|
||||
# 마지막 시도가 아니면 재시도
|
||||
if attempt < recovery_settings.SUNO_MAX_RETRIES:
|
||||
logger.info(f"[Suno] Retrying... ({attempt + 1}/{recovery_settings.SUNO_MAX_RETRIES})")
|
||||
|
||||
# 모든 재시도 실패
|
||||
raise SunoResponseError(
|
||||
f"All {recovery_settings.SUNO_MAX_RETRIES + 1} attempts failed",
|
||||
original_response={"last_error": str(last_error)}
|
||||
)
|
||||
```
|
||||
|
||||
**이유:** 네트워크 오류나 일시적인 서버 오류 시 자동으로 재시도하여 안정성을 높입니다.
|
||||
|
||||
---
|
||||
|
||||
## 5. Creatomate API 커스텀 예외 클래스 추가
|
||||
|
||||
### 파일: `app/utils/creatomate.py`
|
||||
|
||||
**변경 전 (라인 1-20):**
|
||||
```python
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from app.utils.logger import get_logger
|
||||
from config import creatomate_settings
|
||||
|
||||
logger = get_logger("creatomate")
|
||||
```
|
||||
|
||||
**변경 후:**
|
||||
```python
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from app.utils.logger import get_logger
|
||||
from config import creatomate_settings, recovery_settings
|
||||
|
||||
logger = get_logger("creatomate")
|
||||
|
||||
|
||||
class CreatomateResponseError(Exception):
|
||||
"""Creatomate API 응답 오류 시 발생하는 예외
|
||||
|
||||
Creatomate API 렌더링 실패 또는 비정상 응답 시 사용됩니다.
|
||||
재시도 로직에서 이 예외를 catch하여 재시도를 수행합니다.
|
||||
|
||||
Attributes:
|
||||
message: 에러 메시지
|
||||
original_response: 원본 API 응답 (있는 경우)
|
||||
"""
|
||||
def __init__(self, message: str, original_response: dict | None = None):
|
||||
self.message = message
|
||||
self.original_response = original_response
|
||||
super().__init__(self.message)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Creatomate API 타임아웃 적용
|
||||
|
||||
### 파일: `app/utils/creatomate.py`
|
||||
|
||||
**변경 전 (라인 138):**
|
||||
```python
|
||||
self._client = httpx.AsyncClient(
|
||||
base_url=self.BASE_URL,
|
||||
headers=self._get_headers(),
|
||||
timeout=httpx.Timeout(60.0, connect=10.0),
|
||||
)
|
||||
```
|
||||
|
||||
**변경 후:**
|
||||
```python
|
||||
self._client = httpx.AsyncClient(
|
||||
base_url=self.BASE_URL,
|
||||
headers=self._get_headers(),
|
||||
timeout=httpx.Timeout(
|
||||
recovery_settings.CREATOMATE_RENDER_TIMEOUT,
|
||||
connect=recovery_settings.CREATOMATE_CONNECT_TIMEOUT
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
**변경 전 (라인 258, 291):**
|
||||
```python
|
||||
timeout=30.0
|
||||
```
|
||||
|
||||
**변경 후:**
|
||||
```python
|
||||
timeout=recovery_settings.CREATOMATE_DEFAULT_TIMEOUT
|
||||
```
|
||||
|
||||
**변경 전 (라인 446, 457):**
|
||||
```python
|
||||
timeout=60.0
|
||||
```
|
||||
|
||||
**변경 후:**
|
||||
```python
|
||||
timeout=recovery_settings.CREATOMATE_RENDER_TIMEOUT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Creatomate API 재시도 로직 추가
|
||||
|
||||
### 파일: `app/utils/creatomate.py`
|
||||
|
||||
**변경 전 - render_with_json() 메서드 (라인 440~):**
|
||||
```python
|
||||
async def render_with_json(
|
||||
self,
|
||||
template_id: str,
|
||||
modifications: dict[str, Any],
|
||||
task_id: str,
|
||||
) -> dict:
|
||||
"""JSON 수정사항으로 렌더링 요청"""
|
||||
payload = {
|
||||
"template_id": template_id,
|
||||
"modifications": modifications,
|
||||
"webhook_url": f"{creatomate_settings.CREATOMATE_CALLBACK_URL}?task_id={task_id}",
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{self.BASE_URL}/v1/renders",
|
||||
headers=self._get_headers(),
|
||||
json=payload,
|
||||
timeout=60.0,
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"Failed to render: {response.text}")
|
||||
raise Exception(f"Failed to render: {response.status_code}")
|
||||
|
||||
return response.json()
|
||||
```
|
||||
|
||||
**변경 후:**
|
||||
```python
|
||||
async def render_with_json(
|
||||
self,
|
||||
template_id: str,
|
||||
modifications: dict[str, Any],
|
||||
task_id: str,
|
||||
) -> dict:
|
||||
"""JSON 수정사항으로 렌더링 요청 (재시도 로직 포함)
|
||||
|
||||
Args:
|
||||
template_id: Creatomate 템플릿 ID
|
||||
modifications: 수정사항 딕셔너리
|
||||
task_id: 작업 고유 식별자
|
||||
|
||||
Returns:
|
||||
Creatomate API 응답 데이터
|
||||
|
||||
Raises:
|
||||
CreatomateResponseError: API 오류 또는 재시도 실패 시
|
||||
"""
|
||||
payload = {
|
||||
"template_id": template_id,
|
||||
"modifications": modifications,
|
||||
"webhook_url": f"{creatomate_settings.CREATOMATE_CALLBACK_URL}?task_id={task_id}",
|
||||
}
|
||||
|
||||
last_error: Exception | None = None
|
||||
|
||||
for attempt in range(recovery_settings.CREATOMATE_MAX_RETRIES + 1):
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{self.BASE_URL}/v1/renders",
|
||||
headers=self._get_headers(),
|
||||
json=payload,
|
||||
timeout=recovery_settings.CREATOMATE_RENDER_TIMEOUT,
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
|
||||
# 재시도 불가능한 오류 (4xx 클라이언트 오류)
|
||||
if 400 <= response.status_code < 500:
|
||||
raise CreatomateResponseError(
|
||||
f"Client error: {response.status_code}",
|
||||
original_response={"status": response.status_code, "text": response.text}
|
||||
)
|
||||
|
||||
# 재시도 가능한 오류 (5xx 서버 오류)
|
||||
last_error = CreatomateResponseError(
|
||||
f"Server error: {response.status_code}",
|
||||
original_response={"status": response.status_code, "text": response.text}
|
||||
)
|
||||
|
||||
except httpx.TimeoutException as e:
|
||||
logger.warning(f"[Creatomate] Timeout on attempt {attempt + 1}/{recovery_settings.CREATOMATE_MAX_RETRIES + 1}")
|
||||
last_error = e
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.warning(f"[Creatomate] HTTP error on attempt {attempt + 1}: {e}")
|
||||
last_error = e
|
||||
|
||||
# 마지막 시도가 아니면 재시도
|
||||
if attempt < recovery_settings.CREATOMATE_MAX_RETRIES:
|
||||
logger.info(f"[Creatomate] Retrying... ({attempt + 1}/{recovery_settings.CREATOMATE_MAX_RETRIES})")
|
||||
|
||||
# 모든 재시도 실패
|
||||
raise CreatomateResponseError(
|
||||
f"All {recovery_settings.CREATOMATE_MAX_RETRIES + 1} attempts failed",
|
||||
original_response={"last_error": str(last_error)}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 작업 체크리스트
|
||||
|
||||
| 순번 | 작업 내용 | 파일 | 상태 |
|
||||
|------|----------|------|------|
|
||||
| 1 | RecoverySettings에 Suno/Creatomate 설정 추가 | `config.py` | ✅ |
|
||||
| 2 | .env에 타임아웃/재시도 환경변수 추가 | `.env` | ✅ |
|
||||
| 3 | SunoResponseError 예외 클래스 추가 | `app/utils/suno.py` | ✅ |
|
||||
| 4 | Suno 타임아웃 적용 (recovery_settings 사용) | `app/utils/suno.py` | ✅ |
|
||||
| 5 | Suno 재시도 로직 추가 | `app/utils/suno.py` | ✅ |
|
||||
| 6 | CreatomateResponseError 예외 클래스 추가 | `app/utils/creatomate.py` | ✅ |
|
||||
| 7 | Creatomate 타임아웃 적용 (recovery_settings 사용) | `app/utils/creatomate.py` | ✅ |
|
||||
| 8 | Creatomate 재시도 로직 추가 | `app/utils/creatomate.py` | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 참고사항
|
||||
|
||||
- **DB failed 상태 저장**: `song_task.py`와 `video_task.py`에 이미 구현되어 있습니다.
|
||||
- **Response failed 상태**: 모든 스키마에 `success`, `status` 필드가 이미 존재합니다.
|
||||
- 재시도는 5xx 서버 오류와 타임아웃에만 적용되며, 4xx 클라이언트 오류는 즉시 실패 처리합니다.
|
||||
- 모든 타임아웃/재시도 설정은 `RecoverySettings`에서 통합 관리합니다.
|
||||
Loading…
Reference in New Issue