o2o-castad-backend/docs/plan/celery/celery-plan_2-callback-link...

1079 lines
45 KiB
Markdown

# 설계안 2: Celery Callback Link + 에러 격리 파이프라인
> **`link`/`link_error` 콜백과 단일 큐 + 우선순위 라우팅 기반 설계**
---
## 목차
1. [개요 및 핵심 차이점](#1-개요-및-핵심-차이점)
2. [아키텍처 설계](#2-아키텍처-설계)
3. [데이터 흐름 상세](#3-데이터-흐름-상세)
4. [큐 및 태스크 동작 상세](#4-큐-및-태스크-동작-상세)
5. [코드 구현](#5-코드-구현)
6. [상태 관리 및 모니터링](#6-상태-관리-및-모니터링)
7. [실패 처리 전략](#7-실패-처리-전략)
8. [설계 및 동작 설명](#8-설계-및-동작-설명)
9. [기존안과의 비교](#9-기존안과의-비교)
10. [배포 및 운영](#10-배포-및-운영)
---
## 1. 개요 및 핵심 차이점
### 1.1 설계 철학
이 설계안은 **`link`(성공 콜백)과 `link_error`(실패 콜백)**을 활용하여 태스크 간 연결을 구현합니다.
기존안(`celery-plan.md`)은 태스크 내부에서 다음 단계를 `apply_async()`로 직접 호출하고,
설계안 1(`chain`)은 API에서 전체 순서를 선언합니다.
**이 방식은 태스크 발행 시점에 콜백을 등록**하여, 성공/실패에 따른 분기를 태스크 외부에서 제어합니다.
또한 **단일 큐 + 우선순위 라우팅** 전략으로 인프라를 단순화합니다.
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 3가지 방식 비교 │
├─────────────────────────┬───────────────────┬───────────────────────────────┤
│ 기존안 (명시적 전달) │ 설계안 1 (chain) │ 설계안 2 (callback link) │
├─────────────────────────┼───────────────────┼───────────────────────────────┤
│ 태스크 내부에서 │ API에서 전체 순서를│ 태스크 발행 시 성공/실패 │
│ 다음 태스크를 직접 호출 │ chain으로 선언 │ 콜백을 등록 │
│ │ │ │
│ A() 내부에서 B.delay() │ chain(A|B|C) │ A.apply_async( │
│ │ │ link=B.s(), │
│ │ │ link_error=err.s() │
│ │ │ ) │
├─────────────────────────┼───────────────────┼───────────────────────────────┤
│ 단순하지만 강한 결합 │ 선언적이지만 │ 유연한 성공/실패 분기 │
│ │ 부분 재시작 어려움 │ + 단일 큐로 인프라 단순화 │
└─────────────────────────┴───────────────────┴───────────────────────────────┘
```
### 1.2 핵심 원칙
```
┌─────────────────────────────────────────────────────────────────┐
│ 핵심 설계 원칙 │
├─────────────────────────────────────────────────────────────────┤
│ 1. 콜백 기반 연결: link/link_error로 성공/실패 분기 │
│ 2. 단일 큐 + 우선순위: 3개 큐 대신 1개 큐 + priority 사용 │
│ 3. 에러 격리: 각 단계별 독립 에러 핸들러 │
│ 4. 제어 역전: 태스크가 아닌 발행자가 다음 단계를 결정 │
└─────────────────────────────────────────────────────────────────┘
```
### 1.3 단일 큐 + 우선순위 전략
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 큐 전략 비교: 3개 큐 vs 1개 큐 + 우선순위 │
└─────────────────────────────────────────────────────────────────────────────┘
[기존안: 3개 독립 큐]
┌──────────┐ ┌──────────┐ ┌──────────┐
│lyric_q │ │ song_q │ │video_q │
│(Worker1) │ │(Worker2) │ │(Worker3) │
└──────────┘ └──────────┘ └──────────┘
• 장점: 완벽한 격리
• 단점: 워커 3개 항상 가동, 유휴 리소스 낭비
[이 설계안: 단일 큐 + 우선순위]
┌─────────────────────────────────────────┐
│ pipeline_queue │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │Priority │ │Priority │ │Priority │ │
│ │ 10 │ │ 5 │ │ 1 │ │
│ │(lyric) │ │ (song) │ │(video) │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ 모든 워커가 모든 태스크 처리 가능 │
│ 우선순위로 실행 순서 자연스럽게 제어 │
└─────────────────────────────────────────┘
• 장점: 워커 풀 공유, 리소스 효율적
• 단점: 특정 유형의 태스크만 처리하는 워커 불가
```
---
## 2. 아키텍처 설계
### 2.1 전체 아키텍처
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Callback Link + 단일 큐 아키텍처 │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────┐
│ Client │
└──────┬──────┘
┌────────────────────┐
│ FastAPI │
│ │
│ lyric.apply_async( │
│ link=song.s(), │◄── 성공 시 song 실행
│ link_error= │
│ err.s() │◄── 실패 시 에러 핸들러
│ ) │
└─────────┬──────────┘
┌────────────────────────────────────┐
│ pipeline_queue │
│ (단일 큐, 우선순위 지원) │
│ │
│ Priority 10: Lyric 태스크 │
│ Priority 5: Song 태스크 │
│ Priority 1: Video 태스크 │
└───────────────┬────────────────────┘
┌─────────────────────┼─────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Worker 1 │ │ Worker 2 │ │ Worker 3 │
│ (모든 태스크) │ │ (모든 태스크) │ │ (모든 태스크) │
└───────────────┘ └───────────────┘ └───────────────┘
│ │ │
│ 각 워커는 lyric, song, video 모두 처리 가능│
│ 우선순위가 높은 태스크(lyric)를 먼저 실행 │
└─────────────────────┴─────────────────────┘
```
### 2.2 link/link_error 콜백 구조
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ link/link_error 콜백 구조 │
└─────────────────────────────────────────────────────────────────────────────┘
lyric.apply_async(
kwargs={...},
link=song.s(), ◄── 성공 콜백: lyric 성공 → song 실행
link_error=lyric_error.s(), ◄── 실패 콜백: lyric 실패 → 에러 처리
)
내부 동작:
━━━━━━━━━
lyric_task 실행
┌──────────┼──────────┐
│ │
[성공 시] [실패 시]
│ │
▼ ▼
link 콜백 실행 link_error 콜백 실행
= song.s() 실행 = lyric_error.s() 실행
│ │
▼ ▼
song_task 실행 에러 로깅/알림
│ DLQ 저장
┌────┼────┐
│ │
[성공] [실패]
│ │
▼ ▼
video song_error
```
### 2.3 콜백 중첩 (Nested Callbacks)
```python
# 전체 파이프라인을 콜백 중첩으로 표현
lyric_task.apply_async(
kwargs=initial_data,
priority=10, # 높은 우선순위
link=song_task.s().set(
priority=5, # 중간 우선순위
link=video_task.s().set(
priority=1, # 낮은 우선순위
link_error=video_error_handler.s()
),
link_error=song_error_handler.s()
),
link_error=lyric_error_handler.s()
)
# 풀어서 설명:
# lyric 성공 → song 실행
# song 성공 → video 실행
# video 성공 → 파이프라인 완료
# video 실패 → video_error_handler
# song 실패 → song_error_handler
# lyric 실패 → lyric_error_handler
```
---
## 3. 데이터 흐름 상세
### 3.1 시퀀스 다이어그램
```mermaid
sequenceDiagram
participant C as Client
participant API as FastAPI
participant Q as pipeline_queue
participant W1 as Worker (lyric)
participant W2 as Worker (song)
participant W3 as Worker (video)
participant EH as Error Handler
participant DB as MySQL
C->>API: POST /pipeline/start
API->>Q: lyric_task + link(song) + link_error(err)
API-->>C: {"task_id": "xxx"}
Q->>W1: lyric_task (priority=10)
W1->>DB: Lyric 생성 + ChatGPT
alt 성공
W1-->>Q: SUCCESS → link 콜백 발동
Note over Q: Celery가 자동으로 song_task를 큐에 발행
Q->>W2: song_task (priority=5)
W2->>DB: Song 생성 + Suno
alt 성공
W2-->>Q: SUCCESS → link 콜백 발동
Q->>W3: video_task (priority=1)
W3->>DB: Video 생성 + Creatomate
W3-->>Q: SUCCESS
Note over Q: 파이프라인 완료
else 실패
W2-->>EH: link_error → song_error_handler
EH->>DB: 실패 상태 기록
end
else 실패
W1-->>EH: link_error → lyric_error_handler
EH->>DB: 실패 상태 기록
end
```
### 3.2 우선순위 기반 태스크 처리
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 우선순위 기반 태스크 처리 │
└─────────────────────────────────────────────────────────────────────────────┘
시간축 →
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
T+0 pipeline_queue:
┌────────────────────────────────────────────────────────────┐
│ [P=10] lyric_A │ [P=10] lyric_B │ [P=5] song_X │ │
└────────────────────────────────────────────────────────────┘
Worker1 → lyric_A 처리 (P=10, 가장 높은 우선순위)
Worker2 → lyric_B 처리
T+5 lyric_A 완료 → link 콜백으로 song_A가 큐에 추가 (P=5)
┌────────────────────────────────────────────────────────────┐
│ [P=5] song_X │ [P=5] song_A │ │
└────────────────────────────────────────────────────────────┘
Worker1 → song_X 처리 (같은 우선순위면 FIFO)
T+60 song_X 완료 → video_X 추가 (P=1)
┌────────────────────────────────────────────────────────────┐
│ [P=5] song_A │ [P=1] video_X │ │
└────────────────────────────────────────────────────────────┘
Worker1 → song_A 처리 (P=5 > P=1)
※ video_X는 모든 song 태스크보다 낮은 우선순위
효과:
• 새로운 파이프라인 요청(lyric)이 가장 먼저 처리됨
• 이미 진행 중인 파이프라인의 후반 단계(video)는 자연스럽게 후순위
• 전체 처리량(throughput)보다 응답성(latency)을 우선시
```
### 3.3 데이터 전달 방식
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Callback에서의 데이터 전달 │
└─────────────────────────────────────────────────────────────────────────────┘
link 콜백에서 이전 태스크의 결과는 다음 태스크의 첫 번째 인자로 전달됩니다.
이는 chain과 동일한 동작입니다.
lyric_task 반환값:
{
"task_id": "xxx",
"lyric_id": 42,
"status": "completed"
}
│ link 콜백 발동
song_task(prev_result={...}) ◄── 자동으로 첫 번째 인자
song_task 반환값:
{
"task_id": "xxx",
"song_id": 15,
"song_result_url": "...",
"status": "completed"
}
│ link 콜백 발동
video_task(prev_result={...}) ◄── 자동으로 첫 번째 인자
```
---
## 4. 큐 및 태스크 동작 상세
### 4.1 단일 큐 설정
```python
# Redis 큐 우선순위 설정
# Redis는 기본적으로 우선순위 큐를 지원하지 않지만,
# Celery가 priority_steps로 가상 우선순위를 구현합니다.
celery_app.conf.update(
# 브로커 전송 옵션
broker_transport_options={
'priority_steps': list(range(11)), # 0~10 우선순위 레벨
'sep': ':',
'queue_order_strategy': 'priority', # 우선순위 기반 처리
},
)
```
### 4.2 Redis에서의 우선순위 구현
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Redis 우선순위 큐 내부 구조 │
└─────────────────────────────────────────────────────────────────────────────┘
Celery가 Redis에서 우선순위를 구현하는 방법:
priority_steps=[0,1,2,3,4,5,6,7,8,9,10] 설정 시
Redis에 다음과 같은 키가 생성됩니다:
pipeline_queue (priority 0 - 가장 높음)
pipeline_queue\x06\x16\x01 (priority 1)
pipeline_queue\x06\x16\x02 (priority 2)
...
pipeline_queue\x06\x16\x0a (priority 10 - 가장 낮음)
워커가 BRPOP으로 여러 키를 동시에 감시하되,
낮은 번호(높은 우선순위) 키를 먼저 확인합니다.
이 프로젝트에서의 우선순위 할당:
lyric_task → priority=1 (가장 먼저 처리)
song_task → priority=5 (중간)
video_task → priority=9 (가장 나중에)
```
### 4.3 워커 동작
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 단일 큐 워커 동작 │
└─────────────────────────────────────────────────────────────────────────────┘
# 모든 워커가 하나의 큐를 구독
celery -A app.celery_app worker -Q pipeline_queue -c 4
동작 방식:
1. Worker가 pipeline_queue를 BRPOP으로 감시
2. 우선순위 높은 메시지부터 가져옴
3. 메시지의 task 타입에 따라 해당 함수 실행
4. 완료 시 link 콜백이 자동으로 다음 태스크 발행
모든 워커가 모든 태스크를 처리할 수 있으므로:
• 유휴 워커가 없음 (한 유형의 태스크가 없어도 다른 유형 처리)
• 특정 유형에 병목이 생기면 자연스럽게 모든 워커가 분담
• 스케일링이 단순: 워커 수만 조절하면 됨
```
---
## 5. 코드 구현
### 5.1 Celery 앱 설정
```python
# app/celery_app.py
"""
Callback Link + 단일 큐 Celery 설정
핵심 차이점:
- 큐가 1개뿐 (pipeline_queue)
- 우선순위로 태스크 유형별 처리 순서 제어
- 모든 워커가 모든 태스크 처리 가능
"""
from celery import Celery
from kombu import Queue, Exchange
import os
celery_app = Celery(
'o2o_castad',
broker=os.getenv('CELERY_BROKER_URL', 'redis://localhost:6379/0'),
backend=os.getenv('CELERY_RESULT_BACKEND', 'redis://localhost:6379/1'),
include=[
'app.tasks.lyric_tasks',
'app.tasks.song_tasks',
'app.tasks.video_tasks',
'app.tasks.error_handlers', # 에러 핸들러 모듈
]
)
# ============================================================================
# 단일 큐 정의 (우선순위 지원)
# ============================================================================
pipeline_exchange = Exchange('pipeline', type='direct')
celery_app.conf.task_queues = (
Queue(
'pipeline_queue',
pipeline_exchange,
routing_key='pipeline',
queue_arguments={
'x-max-priority': 10, # 우선순위 범위: 0~10
}
),
)
# 기본 큐 설정 - 모든 태스크가 pipeline_queue로 라우팅
celery_app.conf.task_default_queue = 'pipeline_queue'
celery_app.conf.task_default_routing_key = 'pipeline'
# 우선순위 설정
celery_app.conf.broker_transport_options = {
'priority_steps': list(range(11)),
'sep': ':',
'queue_order_strategy': 'priority',
}
celery_app.conf.update(
task_serializer='json',
accept_content=['json'],
result_serializer='json',
timezone='Asia/Seoul',
enable_utc=True,
task_acks_late=True,
task_reject_on_worker_lost=True,
worker_prefetch_multiplier=1,
result_expires=86400,
result_extended=True,
)
```
### 5.2 에러 핸들러 모듈
```python
# app/tasks/error_handlers.py
"""
단계별 에러 핸들러
link_error 콜백으로 사용되는 에러 처리 태스크입니다.
각 단계별로 독립된 에러 핸들러가 있어,
실패 원인에 맞는 대응이 가능합니다.
"""
import logging
import redis
import json
from datetime import datetime
from app.celery_app import celery_app
logger = logging.getLogger(__name__)
redis_client = redis.Redis.from_url(
celery_app.conf.result_backend,
decode_responses=True
)
# ============================================================================
# 공통 에러 핸들러 로직
# ============================================================================
def _handle_error(stage: str, request, exc, traceback):
"""
공통 에러 처리 로직
1. 로깅
2. Redis 상태 업데이트
3. DLQ 저장
4. 알림 발송 (선택)
"""
task_id = 'unknown'
# link_error 콜백에서는 request 객체로 원본 태스크 정보 접근
if hasattr(request, 'kwargs') and request.kwargs:
# 원본 태스크의 kwargs에서 task_id 추출
if isinstance(request.kwargs, dict):
task_id = request.kwargs.get('task_id', 'unknown')
# prev_result에서 추출 (chain/link에서 전달된 경우)
elif hasattr(request, 'args') and request.args:
first_arg = request.args[0]
if isinstance(first_arg, dict):
task_id = first_arg.get('task_id', 'unknown')
logger.error(
f"[{stage.upper()} ERROR] task_id={task_id}, error={exc}",
exc_info=True
)
# Redis 상태 업데이트
pipeline_key = f"pipeline:{task_id}:status"
stage_key = f"pipeline:{task_id}:{stage}"
redis_client.hset(pipeline_key, mapping={
'current_stage': stage,
'status': 'failed',
})
redis_client.hset(stage_key, mapping={
'status': 'failed',
'error': str(exc),
'failed_at': datetime.utcnow().isoformat(),
})
redis_client.expire(pipeline_key, 86400)
redis_client.expire(stage_key, 86400)
# DLQ에 실패 정보 저장
redis_client.lpush('failed_tasks', json.dumps({
'stage': stage,
'task_id': task_id,
'error': str(exc),
'traceback': str(traceback),
'failed_at': datetime.utcnow().isoformat(),
}))
# ============================================================================
# 단계별 에러 핸들러
# ============================================================================
@celery_app.task(name='app.tasks.error_handlers.lyric_error_handler')
def lyric_error_handler(request, exc, traceback):
"""
가사 생성 실패 핸들러
ChatGPT API 실패 원인:
- API 키 만료
- Rate limit 초과
- 네트워크 타임아웃
- 잘못된 프롬프트
"""
_handle_error('lyric', request, exc, traceback)
# 가사 실패 시 추가 처리:
# - API 키 상태 확인 알림 발송
# - Rate limit인 경우 자동 재시도 예약 (선택)
@celery_app.task(name='app.tasks.error_handlers.song_error_handler')
def song_error_handler(request, exc, traceback):
"""
노래 생성 실패 핸들러
Suno API 실패 원인:
- 생성 타임아웃
- Rate limit
- 오디오 다운로드 실패
- Azure 업로드 실패
"""
_handle_error('song', request, exc, traceback)
# 노래 실패 시 추가 처리:
# - 가사는 이미 완료됨 → 재시도 시 가사부터 안해도 됨
# - Suno 크레딧 확인 알림
@celery_app.task(name='app.tasks.error_handlers.video_error_handler')
def video_error_handler(request, exc, traceback):
"""
비디오 생성 실패 핸들러
Creatomate 실패 원인:
- 렌더링 타임아웃
- 템플릿 오류
- 이미지 로드 실패
- Azure 업로드 실패
"""
_handle_error('video', request, exc, traceback)
# 비디오 실패 시 추가 처리:
# - 가사 + 노래 모두 완료 상태 → 비디오만 재시도 가능
# - Creatomate 크레딧 확인 알림
```
### 5.3 가사 생성 태스크
```python
# app/tasks/lyric_tasks.py
"""
가사 생성 태스크 (Callback Link 방식)
chain 방식과 유사하게, 이 태스크는 다음 단계를 모릅니다.
결과를 반환하면 link 콜백이 자동으로 다음 태스크를 실행합니다.
핵심 차이점: 우선순위 설정 (priority=1, 가장 높음)
"""
from sqlalchemy import select
import asyncio
import logging
from app.celery_app import celery_app
from app.tasks.base import BaseTaskWithDB
from app.home.models import Project
from app.lyric.models import Lyric
from app.utils.chatgpt_prompt import ChatgptService
from app.utils.prompts.prompts import Prompt
logger = logging.getLogger(__name__)
@celery_app.task(
base=BaseTaskWithDB,
bind=True,
name='app.tasks.lyric_tasks.generate_lyric',
max_retries=3,
default_retry_delay=30,
acks_late=True,
# ────────────────────────────────────────────
# 단일 큐에서는 큐 이름 대신 우선순위로 구분
# priority=1 → 가장 먼저 처리
# ────────────────────────────────────────────
)
def generate_lyric(self, data: dict) -> dict:
"""
가사 생성 태스크
반환값이 link 콜백(song_task)의 첫 번째 인자가 됩니다.
Args:
data: 초기 파이프라인 데이터
Returns:
dict: 다음 태스크에 전달될 결과
"""
task_id = data['task_id']
self.update_pipeline_status(
task_id=task_id, stage='lyric', status='processing',
message='가사 생성 시작'
)
async def _generate():
async with self.get_db_session() as session:
project = await session.scalar(
select(Project).where(Project.task_id == task_id)
)
if not project:
project = Project(
task_id=task_id,
customer_name=data['customer_name'],
region=data['region'],
)
session.add(project)
await session.flush()
prompt = Prompt(
customer_name=data['customer_name'],
region=data['region'],
detail_region_info=data['detail_region_info'],
language=data.get('language', 'Korean'),
)
lyric_prompt = prompt.get_full_prompt()
lyric = Lyric(
project_id=project.id,
task_id=task_id,
status='processing',
lyric_prompt=lyric_prompt,
language=data.get('language', 'Korean'),
)
session.add(lyric)
await session.commit()
lyric_id = lyric.id
# ChatGPT 호출 (DB 세션 외부)
try:
chatgpt = ChatgptService()
lyric_result = await chatgpt.generate_lyric(lyric_prompt)
if not lyric_result or len(lyric_result.strip()) < 50:
raise ValueError("가사가 너무 짧음")
except Exception as e:
async with self.get_db_session() as session:
lyric = await session.get(Lyric, lyric_id)
lyric.status = 'failed'
lyric.lyric_result = f"Error: {str(e)}"
await session.commit()
raise
# 결과 저장
async with self.get_db_session() as session:
lyric = await session.get(Lyric, lyric_id)
lyric.status = 'completed'
lyric.lyric_result = lyric_result
await session.commit()
return {
'task_id': task_id,
'lyric_id': lyric_id,
'lyric_result': lyric_result,
'language': data.get('language', 'Korean'),
'status': 'completed',
}
result = self.run_async(_generate())
self.update_pipeline_status(
task_id=task_id, stage='lyric', status='completed',
message='가사 생성 완료'
)
# ────────────────────────────────────────────────────────────
# 다음 태스크를 직접 호출하지 않음!
# link 콜백이 자동으로 이 결과를 song_task에 전달합니다.
# ────────────────────────────────────────────────────────────
return result
```
### 5.4 노래/비디오 태스크
노래 및 비디오 태스크는 설계안 1(chain)과 구조적으로 동일합니다.
`prev_result`를 첫 번째 인자로 받아 처리하고, 결과를 반환합니다.
> 설계안 1의 `song_tasks.py`, `video_tasks.py`를 그대로 사용합니다.
> 유일한 차이: `queue` 지정이 없음 (단일 큐이므로)
### 5.5 파이프라인 API (핵심 차이점)
```python
# app/api/routers/v1/pipeline.py
"""
Callback Link 기반 파이프라인 API
핵심 차이점:
1. chain() 대신 link/link_error 콜백 사용
2. 단일 큐에 우선순위로 발행
3. 각 단계별 독립 에러 핸들러 등록
"""
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from app.dependencies.auth import get_current_user
from app.tasks.lyric_tasks import generate_lyric
from app.tasks.song_tasks import generate_song
from app.tasks.video_tasks import generate_video
from app.tasks.error_handlers import (
lyric_error_handler,
song_error_handler,
video_error_handler,
)
router = APIRouter(prefix="/pipeline", tags=["Pipeline"])
class StartPipelineRequest(BaseModel):
task_id: str
customer_name: str
region: str
detail_region_info: str
language: str = "Korean"
orientation: str = "vertical"
genre: str = "pop, ambient"
@router.post("/start")
async def start_pipeline(
request: StartPipelineRequest,
current_user=Depends(get_current_user)
):
"""
파이프라인 시작 (Callback Link 방식)
link/link_error를 중첩하여 전체 파이프라인을 구성합니다.
각 단계별로 독립된 에러 핸들러가 등록됩니다.
"""
initial_data = {
"task_id": request.task_id,
"customer_name": request.customer_name,
"region": request.region,
"detail_region_info": request.detail_region_info,
"language": request.language,
}
# ────────────────────────────────────────────────────────────────
# 핵심: 콜백 중첩으로 파이프라인 구성
# ────────────────────────────────────────────────────────────────
#
# 읽는 순서: 안쪽부터 바깥으로
# video_task ← song 성공 시 실행 (priority=9)
# song_task ← lyric 성공 시 실행 (priority=5)
# lyric_task ← 즉시 실행 (priority=1)
#
# 에러 핸들러: 각 단계별 독립
# lyric 실패 → lyric_error_handler
# song 실패 → song_error_handler
# video 실패 → video_error_handler
result = generate_lyric.apply_async(
kwargs={'data': initial_data},
priority=1, # 가장 높은 우선순위
# lyric 성공 시 → song 실행
link=generate_song.s(
genre=request.genre,
).set(
priority=5, # 중간 우선순위
# song 성공 시 → video 실행
link=generate_video.s(
orientation=request.orientation,
).set(
priority=9, # 가장 낮은 우선순위
link_error=video_error_handler.s(), # video 실패 시
),
link_error=song_error_handler.s(), # song 실패 시
),
link_error=lyric_error_handler.s(), # lyric 실패 시
)
return {
'success': True,
'task_id': request.task_id,
'celery_task_id': result.id,
'message': '파이프라인이 시작되었습니다.',
}
@router.post("/retry/{task_id}/{stage}")
async def retry_stage(
task_id: str,
stage: str,
current_user=Depends(get_current_user)
):
"""
특정 단계 재시도
실패한 단계부터 콜백을 다시 구성하여 재시작합니다.
"""
if stage == 'song':
# DB에서 lyric 결과 조회 후 song부터 재시작
lyric_result = await _get_lyric_result(task_id)
result = generate_song.apply_async(
args=(lyric_result,),
priority=5,
link=generate_video.s().set(
priority=9,
link_error=video_error_handler.s(),
),
link_error=song_error_handler.s(),
)
elif stage == 'video':
song_result = await _get_song_result(task_id)
result = generate_video.apply_async(
args=(song_result,),
priority=9,
link_error=video_error_handler.s(),
)
return {'resumed_from': stage, 'task_id': result.id}
```
---
## 6. 상태 관리 및 모니터링
### 6.1 상태 추적
기존안의 Redis 커스텀 상태(`pipeline:{task_id}:*`)를 그대로 사용합니다.
추가로 에러 핸들러에서도 상태를 업데이트합니다.
### 6.2 Flower 모니터링
```bash
# 단일 큐이므로 더 단순한 모니터링
celery -A app.celery_app flower --port=5555
# Flower에서 볼 수 있는 정보:
# - pipeline_queue의 대기 태스크 수 (전체)
# - 태스크 유형별 필터링 (lyric/song/video)
# - 우선순위별 분포
```
---
## 7. 실패 처리 전략
### 7.1 단계별 독립 에러 핸들링
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 에러 격리 전략 │
└─────────────────────────────────────────────────────────────────────────────┘
이 방식의 가장 큰 장점: 각 단계별로 독립된 에러 핸들러
[기존안/chain]
─────────────
모든 실패 → 동일한 에러 처리 또는 chain 자체 실패
[이 방식]
─────────
lyric 실패 → lyric_error_handler
├─ ChatGPT API 키 확인
├─ Rate limit 대기 후 재시도 예약
└─ 관리자에게 API 상태 알림
song 실패 → song_error_handler
├─ Suno 크레딧 확인
├─ 이미 완료된 lyric 보존
└─ song만 재시도 가능
video 실패 → video_error_handler
├─ Creatomate 렌더링 오류 분석
├─ 이미 완료된 lyric + song 보존
└─ video만 재시도 가능
각 에러 핸들러는 해당 단계의 실패 원인에 맞는 구체적인 대응이 가능합니다.
이것이 "에러 격리"의 핵심입니다.
```
---
## 8. 설계 및 동작 설명
### 8.1 link vs chain vs 명시적 전달 상세 비교
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 태스크가 다음 단계를 "아는가"에 따른 분류 │
└─────────────────────────────────────────────────────────────────────────────┘
태스크가 다음 단계를 안다?
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
[안다 (Yes)] [모른다 (No)] [모른다 (No)]
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌───────────┐ ┌──────────────┐
│ 기존안 │ │ 설계안 1 │ │ 설계안 2 │
│ (명시적 전달)│ │ (chain) │ │ (link 콜백) │
└─────────────┘ └───────────┘ └──────────────┘
│ │ │
▼ ▼ ▼
다음 단계 지식이 API에서 chain API에서 link
태스크 코드에 선언 콜백으로 연결
하드코딩 (파이프라인만) (에러 핸들러도
단계별 등록)
```
### 8.2 단일 큐의 장단점
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 단일 큐 장단점 분석 │
└─────────────────────────────────────────────────────────────────────────────┘
장점:
─────
✓ 인프라 단순화: Redis 큐 1개, 워커 타입 1개
✓ 리소스 효율: 유휴 워커 없음, 모든 워커가 모든 작업 처리
✓ 스케일링 단순: 워커 수만 조절
✓ 운영 부담 감소: 워커 3종류 → 1종류 관리
단점:
─────
✗ 격리 불가: video 작업이 lyric 워커 리소스 사용 가능
✗ 리소스 불균형: ChatGPT(가벼움)와 Creatomate(무거움) 같은 워커
✗ 우선순위 한계: Redis 우선순위가 RabbitMQ만큼 정확하지 않음
✗ 특정 워커 튜닝 불가: 메모리, CPU 한도를 태스크 유형별로 설정 불가
이 프로젝트에서의 판단:
──────────────────────
• 트래픽이 적은 초기 단계에서는 단일 큐가 효율적
• 트래픽 증가 시 3개 큐로 전환 가능 (호환됨)
• 우선순위로 "새 요청 우선 처리"가 가능하여 UX 향상
```
---
## 9. 기존안과의 비교
```
┌──────────────────────┬──────────────┬──────────────┬──────────────┐
│ 기준 │ 기존안 │ 설계안 1 │ 설계안 2 │
│ │ (명시적 전달)│ (chain) │ (link+단일큐)│
├──────────────────────┼──────────────┼──────────────┼──────────────┤
│ 큐 구성 │ 3개 독립 큐 │ 3개 독립 큐 │ 1개 우선순위 │
│ 워커 구성 │ 3종류 │ 3종류 │ 1종류 │
│ 태스크 독립성 │ 중간 │ 높음 │ 높음 │
│ 에러 핸들링 │ 태스크 내 │ link_error │ 단계별 독립 │
│ 부분 재시작 │ 쉬움 │ 약간 번거로움│ 쉬움 │
│ 인프라 복잡도 │ 중간 │ 중간 │ 낮음 │
│ 리소스 효율 │ 중간 │ 중간 │ 높음 │
│ 적합한 규모 │ 중~대 │ 중 │ 소~중 │
│ Celery 숙련도 │ 초급 │ 중급 │ 중급 │
└──────────────────────┴──────────────┴──────────────┴──────────────┘
```
---
## 10. 배포 및 운영
### 10.1 실행 명령어
```bash
# 워커 실행 - 모든 워커가 동일한 큐 구독
# 워커 수만 조절하면 됨
celery -A app.celery_app worker -Q pipeline_queue -c 4 -n worker1@%h
celery -A app.celery_app worker -Q pipeline_queue -c 4 -n worker2@%h
celery -A app.celery_app worker -Q pipeline_queue -c 4 -n worker3@%h
# 또는 단일 워커로 시작 (개발환경)
celery -A app.celery_app worker -Q pipeline_queue -c 4 -n dev@%h
```
### 10.2 Docker Compose
```yaml
# docker-compose.yml (단순화됨)
version: '3.8'
services:
redis:
image: redis:7-alpine
ports: ["6379:6379"]
api:
build: .
ports: ["8000:8000"]
command: uvicorn main:app --host 0.0.0.0 --port 8000
# 워커가 1종류뿐이므로 스케일링이 단순
worker:
build: .
command: celery -A app.celery_app worker -Q pipeline_queue -c 4
deploy:
replicas: 3 # 워커 3개 (모든 태스크 처리)
```
---
## 문서 버전
| 버전 | 날짜 | 변경 내용 |
|------|------|-----------|
| 1.0 | 2024-XX-XX | 초안 작성 |